Iteration
Haskell doesn't have the while and for loops that we have used in imperative languages. Such loops are conditioned on mutable counters or iterators. To repeat code in Haskell, we must write a recursive function because it is only through function calls that we effect state changes. We just saw this recursion with getInt
. Let's look at a few more examples.
Suppose we are prompting the user with a multiple choice question, and we expect them to enter A, B, C, or D. If they enter something else, we repeat the prompt. To implement this loop in Haskell, we factor out the action into this repeatable function.
getChoice :: IO Char
getChoice = do
letter <- getLine
if letter == "A" || letter == "B" || letter == "C" || letter == "D" then
return (head letter)
else do
putStr "That wasn't an option. Try again: "
getChoice
The function first grabs the line of input. Then it checks to see if the user entered one of the valid options. If so, it returns the input as an IO Char
. If not, it recurses.
In an imperative language, we'd write getChoice
as a do-while loop. Such loops have this general iterative structure:
do
action
while condition
In a functional language with immutable data, these loops have this general recursive structure:
function doWhile
action
if criteria met
return
else
recurse
Suppose we are translating an imperative loop to a pure recursive function, and the imperative loop is conditioned on a count or some other mutating state. We must migrate the state to the parameters of the recursive function. By putting it in the parameters, the state can effectively mutate from call to call. For example, this Ruby function implements a counted repeat loop via an iteration index passed along as a parameter:
def countdown(n)
if n == 0
return
else
puts n
repeat(n - 1)
The recursive call receives n - 1
, and the repetition stops when the parameter reaches 0. No data is actually mutated in this code, but it achieves the same results as an imperative loop mutating a memory cell on a von Neumann architecture.
Normally we wouldn't write a conditional statement with a then-block that does nothing. Not in Ruby, anyway. However, the advantage to writing countdown
this way is it translates more or less line-for-line to a Haskell function:
countdown :: Int -> IO ()
countdown n = do
if n == 0 then
return ()
else do
print n
countdown (n - 1)
Conditional expressions in Haskell must always have both then and else branches, even if they are just performing IO actions.
Note that the body of the else branch has two statements, but the then branch has only one. Any time we have more than a single statement, we need to sequence the statements together in a do
block. As we can see, the else branch has its own nested do
block, but the then branch does not. The do
keyword is easy to forget, and the resulting errors are difficult to decipher. Be careful.
Though Haskell lacks traditional loops, its system of lazy evaluation means we can write a loop abstraction as a higher-order function. This repeat
function is a generalization of countdown
that takes an extra parameter for the action to repeat:
repeat :: Int -> IO () -> IO ()
repeat n action = do
if n == 0 then
return ()
else do
action
repeat (n - 1)
Callers invoke this abstraction by passing along a count and the action to repeat:
main = do
repeat 5 (putStrLn "play")
repeat 5 (putStrLn "hate")
repeat 5 (putStrLn "shake")
There. Now Haskell has loops.