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?

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.