Acrostic

Dear Computer

Chapter 7: Immutability and I/O

Acrostic

Suppose you're writing a program that prompts users to write an acrostic poem. The theme of the poem is passed as a command-line argument, and then the program prompts the user to enter words that start with the theme's letters. For example, here's an acrostic that the user might enter for the theme COFFEE:

Shell
> runhaskell acrostic.hs COFFEE
Costly
Overpriced
Fanatics
Fetish
Expensive
Elitism

The program prints the first letters. The user only inputs ostly, verpriced, and so on.

The first task is to get the theme from the command-line arguments. The getArgs function from the System.Environment module provides these and has this type signature:

Haskell
getArgs :: IO [String]

Separate runs of the program will have different command-line arguments. Because it doesn't give back the same result every single time you call it, the getArgs function is impure. Its returned value must be unpacked with the <- operator inside a do block:

Haskell
import System.Environment

main = do
  args <- getArgs
  let theme = head args
  print theme

Next we need a function that recurses through the theme, one character at a time. When the theme is entirely consumed, we end the recursion with a return (). Otherwise we print the first character, prompt the user to fill in a word that starts with that character, and then recurse on the remaining characters. The cue function in this program does the trick:

Haskell
import System.Environment

cue :: String -> IO ()
cue theme =
  if theme == "" then
    return ()
  else do
    putChar (head theme)
    getLine
    cue (tail theme)

main = do
  args <- getArgs
  let theme = head args
  cue theme

Summary

Our imperative programming experiences suggest that mutability is necessary for computation, but Haskell demonstrates that it is not. Many functions are pure, meaning that their behavior is entirely predetermined by their parameters. They do not rely on persistent and modifiable state. Functions that do rely on and effect state changes, like I/O and random number generators, may be written to accept the incoming state as a parameter and return the new outgoing state. Haskell's do notation hides the ugliness of passing around this modified state. It doesn't hide everything, however. Data returned from an impure function is boxed up in an IO wrapper. The box may only be opened inside an impure context like another do block. Using do and IO, programmers write Haskell programs that look similar to the ones they'd write in imperative languages, but without the dangers of mutability.

← Guessing GameLecture: Haskell Mains →