Iteration

Dear Computer

Chapter 7: Immutability and I/O

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.

Haskell
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:

Ruby
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:

Haskell
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:

Haskell
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:

Haskell
main = do
  repeat 5 (putStrLn "play")
  repeat 5 (putStrLn "hate")
  repeat 5 (putStrLn "shake")

There. Now Haskell has loops.

← Maybe DataFiles →