6
\$\begingroup\$

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?

\$\endgroup\$
5
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
5
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.