I started learning Haskell a couple of days ago and decided to build a Caesar shift in it.
import Data.Char
shift_str :: Int -> ([Char] -> [Char])
shift_str num
| num > 1 = \str -> shift_str_forwards (shift_str (num-1) str)
| num == 1 = shift_str_forwards
| num == 0 = no_shift
| num == -1 = shift_str_backwards
| num < -1 = \str -> shift_str_backwards (shift_str (num+1) str)
no_shift :: [Char] -> [Char]
no_shift str = str
shift_str_forwards :: [Char] -> [Char]
shift_str_forwards str = map shift_forwards str
shift_str_backwards :: [Char] -> [Char]
shift_str_backwards str = map shift_backwards str
shift_forwards :: Char -> Char
shift_forwards char
| char == 'z' = 'a'
| otherwise = chr (1 + ord char)
shift_backwards :: Char -> Char
shift_backwards char
| char == 'a' = 'z'
| otherwise = chr (ord char - 1)
However, this seems far too complex to be the right way of doing it. Any advice?
2 Answers 2
First of all, it's great that you use type signatures. However, your using snake_case
, whereas Haskell usually uses camelCase
for functions, e.g. no_shift
should be called noShift
.
Next, your shift functions for characters can be simplified by both pattern matching and succ
and pred
:
shiftForwards :: Char -> Char
shiftForwards 'z' = 'a'
shiftForwards c = succ c
shiftBackwards :: Char -> Char
shiftBackwards 'a' = 'z'
shiftBackwards c = pred c
Next, your "global" function gets easier if you use str
as an argument and not in a lambda.
shiftStr :: Int -> [Char] -> [Char]
shiftStr num str
| num > 0 = shiftStr (num - 1) (map shiftForwards str)
| num < 0 = shiftStr (num + 1) (map shiftBackwards str)
| num == 0 = str
The special cases for num == 1
and num == -1
aren't necessary, since shiftStr
will return immediately if the subsequent call uses num = 0
.
shiftStr
'encrypts' each letter of input string independently of other letters. This could be emphasized by using map
at the top of shiftStr
instead of hiding it in helper functions:
shiftStr :: Int -> String -> String
shiftStr n xs = map (shiftChar n) xs
Or in pointfree style without variables:
shiftStr :: Int -> String -> String
shiftStr = map . shiftChar
shiftChar n c
replaces letter by searching for another one n
positions away. I'll implement this literally by creating cycled alphabet and searching for letters in it. This is inefficient but allows to get taste of laziness.
alphabet = ['a'..'z'] :: String
alphaLoop = cycle alphabet :: String
shiftChar :: Int -> Char -> Char
shiftChar n c = head $ drop n $ dropWhile (/= c) alphaLoop
cycle
creates cycle from a list. dropWhile
skips some characters and returns list starting from c
.
Try it in ghci (alphaLoop
is infinite so be careful with it).
> let alphaLoop = cycle ['a'..'z']
> take 30 alphaLoop
"abcdefghijklmnopqrstuvwxyzabcd"
> take 30 $ dropWhile (/= 'x') alphaLoop
"xyzabcdefghijklmnopqrstuvwxyza"
> take 30 $ drop 1 $ dropWhile (/= 'x') alphaLoop
"yzabcdefghijklmnopqrstuvwxyzab"
alphaLoop
is a loop, so we can skip length alphabet - 10
letters instead of moving 10 letters backwards:
shiftChar :: Int -> Char -> Char
shiftChar n c = head $ drop (length alpha + n) $ dropWhile (/= c) alphaLoop
Full code is like this:
shiftStr :: Int -> String -> String
shiftStr n = map shiftChar
where
alphabet = ['a'..'z']
alphaLoop = cycle alphabet
shiftChar c = head
$ drop (length alphabet + n)
$ dropWhile (/= c) alphaLoop
Now try shiftStr 13 "ABC"
and see if you can explain its behavior.