Haskell/Understanding monads/IO

As you should have picked up by now, Haskell is a functional and lazy language. This has some dire consequences for something apparently simple like input/output, but we can solve this with the IO monad.

The Problem: Input/Output and PurityEdit

Haskell functions are in general pure functions: when given the same arguments, they return the same results. The reason for this paradigm is that pure functions are much easier to debug and to prove correct. Test cases can also be set up much more easily, since we can be sure that nothing other than the arguments will influence a function's result. We also require pure functions not to have side effects other than returning a value: a pure function must be self-contained, and cannot open a network connection, write a file or do anything other than producing its result. This allows the Haskell compiler to optimise the code very aggressively.

However, there are very useful functions that cannot be pure: an input function, say getLine, will return different results every time it is called; indeed, that's the point of an input function, since an input function returning always the same result would be pointless. Output operations have side effects, such as creating files or printing strings on the terminal: this is also a violation of purity, because the function is no longer self-contained.

Unwilling to drop the purity of standard functions, but unable to do without impure ones, Haskell places the latter ones in the IO monad. In other words, what we up to now have called "IO actions" are just values in the IO monad.

The IO monad is built in such a way as to be "closed", that is, it is not possible to make a String out of a IO String. Such an extraction would be possible for other monads, such as Maybe String or [String], but not for IO String. In this way, any operation involving an impure function will be "tainted" with the IO monad, which then functions as a signal: if the IO monad is present in a function's signature, we know that that function may have side effects or may produce different results with the same inputs.

Another advantage of using monads is that, by concatenating I/O operations with the (>>=) or (>>) operators, we provide an order in which these I/O operations will be executed. This is important because Haskell is a lazy language, and can decide to evaluate functions whenever the compiler decides it is appropriate: however, this can work only for pure functions! If an operation with side effects (say, writing a log file) were to be written lazily, its entries could be in just about any order: clearly not the result we desire. Locking I/O operations inside a monad allows to define a clear operating sequence.

Combining Pure Functions and Input/OutputEdit

If all useful operations entail input/output, why do we bother with pure functions? The reason is that, thanks to monad properties, we can still have pure functions doing the heavy work, and benefit from the ease with which they can be debugged, tested, proved correct and optimised, while we use the IO monad to get our data and deliver our results.

Let's try a simple example: suppose we have a function converting a string to upper case:

> let shout = map Data.Char.toUpper

The type of this function is clearly pure:

> :t shout
shout :: [Char] -> [Char]

Suppose you apply this function to a string with many repeated characters, for example:

> shout "aaaaaaaaaaaaagh!"

The Haskell compiler needs to apply the function only four times: for 'a', 'g', 'h' and '!'. A C compiler or a Perl interpreter, knowing nothing about purity or self-containment, would have to call the function for all 16 characters, since they cannot be sure that the function will not change its output somehow. The shout function is really trivial, but remember that this line of reasoning is valid for any pure function, and this optimisation capability will be extremely valuable for more complex operations: suppose, for instance, that you had a function to render a character in a particular font, which is a much more expensive operation.

To combine shout with I/O, we ask the user to insert a string (side effect: we are writing to screen), we read it (impure: result can be different every time), apply the (pure) function with liftM and, finally, write the resulting string (again, side effect).

> putStr "Write your string: " >> liftM shout getLine >>= putStrLn
Write your string: This is my string!

IO factsEdit

  • The do notation is especially popular with the IO monad, since it is not possible to extract values from it, and at the same time it is very convenient to use statements with <- to store input values that have to be used multiple times after having been acquired. The above example could be written in do notation as:
do putStr "Write your string: "
   string <- getLine
   putStrLn (shout string)
  • Every Haskell program starts from the main function, which has type IO (). Therefore, in reality, every line of Haskell code you have ever run has run in conjunction with the IO monad!
  • A way of viewing the IO monad is thinking of an IO a value as a computation which gives a value of type a while changing the state of the world by doing input and output. This state of the world is hidden from you by the IO monad, and obviously you cannot set it. Seen this way, IO is roughly analogous to the State monad, which we will meet shortly. With State, however, the state being changed is made of normal Haskell values, and so we can manipulate it directly with pure functions.
  • Actually, there is a "back door" out of the IO monad, System.IO.Unsafe.unsafePerformIO, which will transform, say, a IO String into a String. The naming of the function should point out clearly enough that it is usually not a good idea to use it.
  • Naturally, the standard library has many useful functions for performing I/O, all of them involving the IO monad. Several important ones are presented in the IO chapter in Haskell in Practice.

Monadic control structuresEdit

Given that monads allow us to express sequential execution of actions in a wholly general way, we would hope to use them to implement common iterative patterns, such as loops, in an elegant manner. In this section, we will present a few of the functions from the standard libraries which allow us to do precisely that. While the examples are being presented amidst a discussion on IO for more immediate appeal, keep in mind that what we demonstrate here applies to every monad.

We begin by emphasizing that there is nothing magical about monadic values; we can manipulate them just like any other values in Haskell. Knowing that, we might try to, for instance, use the following to get five lines of user input:

fiveGetLines = replicate 5 getLine

That won't do, however (try it in GHCi!). The problem is that replicate produces, in this case, a list of actions, while we want an action which returns a list (that is, IO [String] rather than [IO String]). What we need is a fold which runs down the list of actions, executing them and combining the results into a single list. As it happens, there is a Prelude function which does that: sequence.

sequence :: (Monad m) => [m a] -> m [a]

And so, we get the desired action.

fiveGetLines = sequence $ replicate 5 getLine

replicate and sequence form an appealing combination; and so Control.Monad offers a replicateM function for repeating an action an arbitrary number of times. Control.Monad provides a number of other convenience functions in the same spirit - monadic zips, folds, and so forth.

fiveGetLinesAlt = replicateM 5 getLine

A particularly important combination is that of map and sequence; it allows us to make actions from a list of values, run them sequentially and collect the results. mapM, a Prelude function, captures this pattern:

mapM :: (Monad m) => (a -> m b) -> [a] -> m [b]

Also common are the variants of the above functions with a trailing underscore in the name, such as sequence_, mapM_ and replicateM_. They discard the return values, and so are appropriate when you are only interested in the side effects (they are to their underscore-less counterparts what (>>) is to (>>=)). mapM_ for instance has the following type:

mapM_ :: (Monad m) => (a -> m b) -> [a] -> m ()

Finally, it is worth mentioning that Control.Monad also provides forM and forM_, which are flipped versions of mapM and mapM_. forM_ happens to be the idiomatic Haskell counterpart to the imperative for-each loop; and the type signature suggests that neatly:

forM_ :: (Monad m) => [a] -> (a -> m b) -> m ()

  1. Using the monadic functions we have just introduced, write a function which prints an arbitrary list of values.
  2. Generalize the bunny invasion example in the list monad chapter for an arbitrary number of generations.
  3. What is the expected behaviour of sequence for the Maybe monad?

Last modified on 18 October 2013, at 02:52