MonoAlphabetic and PolyAlphabetic ciphers in Haskell

This code defines functions to cipher text with both mono-alphetic (Caesar) ciphers and poly-alphabetic ciphers. I also define a shortcut for rot13. I like this code and I think it is easy to read, but code is never perfect so I ask you for improvements.

import Data.Char (toLower)

alphabet :: String
alphabet = ['a'..'z']

generalShift :: String -> Int -> Char -> Char
generalShift alphabet 0 letter = letter
generalShift alphabet positions letter
| letter == last alphabet = generalShift alphabet (positions - 1) (head alphabet)
| otherwise = generalShift alphabet (positions - 1) (succ letter)

shift :: Int -> Char -> Char
shift = generalShift alphabet

clean :: String -> String
clean = filter (elem alphabet) . (map toLower)

monoAlphabeticCipher :: Int -> String -> String
monoAlphabeticCipher key = map (shift key) . clean

rot13 :: String -> String
rot13 = monoAlphabeticCipher 13

poliAlphabeticCipher :: [Int] -> String -> String
poliAlphabeticCipher keys text = zipWith shift keys (clean text)

main :: IO()
main = print \$ poliAlphabeticCipher [3, 2, 1] "abc"


generalShift

Note the following about generalShift:

1. the alphabet argument never changes throughout the recursive calls
2. the only elements of alphabet accessed are the first and last characters
3. last is a potentially expensive function since it traverses the entire list to find the last element

To elaborate on point #2, note that the following calls are exactly the same:

generalShift "abcdefg...xyz" 10 'x'
generalShift "az" 10 'x'


Given these observations, why not just pass the first and last characters of the alphabet to generalShift, i.e.:

generalShift :: Char -> Char -> Int -> Char -> Char
generalShift firstChar lastChar positions letter = ...


elem

elem is another potential expensive call. I would detect alphabetic characters using <= comparisons with the end points of the upper- and lowercase character ranges:

isAlphabetic ch = ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z')

clean str = filter isAlphaBetic . map toLower
-- or the other way around: map toLower . filter isAlphabetic


more generalShift

There is a much faster way to compute the shift of a letter using the fact that Char is an enumeration (i.e. an instance of the Enum class) and therefore the following functions are available:

fromEnum :: Char -> Int
toEnum :: Int -> Char


These work like the ord() and chr() functions in other languages.

Let's assume the first and last characters of the alphabet are the standard a and z. I've leave you to figure out exactly what the code should look like, but here are some hints:

To shift a letter _n_ positions in the range a..z:

1. Determine how far away from 'a' the letter is.
2. Add n to that distance.
3. If the distance computed in step 2 would place it beyond 'z',
subtract the number of characters between 'a' and 'z' and
try again.
4. Add the distance computed in step 3 to the letter 'a'
and return it as the encoded letter.


There will be a bunch of calls to fromEnum and toEnum as well as perhaps the mod function might help here.

poliAlphabeticCipher

Nothing really wrong here, but I have a suggestion... it is customary to repeat the key if it isn't long enough for the entire message. I.e., if the key was [3,2,1] and the message was "Hello", you would repeat the key as many times as was needed to cover every letter of the message. The key used to encode the message in this case would be [3,2,1,3,2].

You can use the cycle function to repeat a list forever:

cycle :: [a] -> [a]
-- e.g. cycle [1,4,2] = [1,4,2,1,4,2,1,4,2,1,4,2...]


and then polyiAlphabeticCipher may be written as:

poliAlphabeticCipher keys text = zipWith shift (cycle keys) (clean text)


With that change monoAlphabeticCipher may be implemented using poliAlphabeticCipher:

monoAlphabeticCipher shift text = poliAlphabeticCipher [shift] text