Haskell/Understanding monads/IO

Two defining features of Haskell are pure functions and lazy evaluation. All Haskell functions are pure, which means that, when given the same arguments, they return the same results. Lazy evaluation means that, by default, Haskell values are only evaluated when some part of the program requires them – perhaps never, if they are never used – and repeated evaluation of the same value is avoided wherever possible.

Pure functions and lazy evaluation bring forth a number of advantages. In particular, pure functions are reliable and predictable; they ease debugging and validation. Test cases can also be set up easily since we can be sure that nothing other than the arguments will influence a function's result. Being entirely contained within the program, the Haskell compiler can evaluate functions thoroughly in order to optimize the compiled code. However, input and output operations, which involve interaction with the world outside the confines of the program, can't be expressed through pure functions. Furthermore, in most cases I/O can't be done lazily. Since lazy computations are only performed when their values become necessary, unfettered lazy I/O would make the order of execution of the real world effects unpredictable.

There is no way to ignore this issue, as any useful program needs to do I/O, even if it is only to display a result. That being so, how do we manage actions like opening a network connection, writing a file, reading input from the outside world, or anything else that goes beyond calculating a value? The main insight is: actions are not functions. The  type constructor provides a way to represent actions as Haskell values, so that we can manipulate them with pure functions. In the Prologue chapter, we anticipated some of the key features of this solution. Now that we also know that  is a monad, we can wrap up the discussion we started there.

Combining functions and I/O actions
Let's combine functions with I/O to create a full program that will:


 * 1) Ask the user to insert a string
 * 2) Read their string
 * 3) Use   to apply a function   that capitalizes all the letters from the string
 * 4) Write the resulting string

We have a full-blown program, but we didn't include any type definitions. Which parts are functions and which are IO actions or other values? We can load our program in GHCi and check the types:

Whew, that is a lot of information there. We've seen all of this before, but let's review.

is. That's not a function. Functions are of types. Our entire program is an IO action.

is a function, but it results in an IO action. The "Write your string: " text is a  (remember, that's just a synonym for  ). It is used as an argument for  and is incorporated into the IO action that results. So,  is a function, but   evaluates to an IO action. The  part of the IO type (called a unit type) indicates that nothing is available to be passed on to any later functions or actions.

That last part is key. We sometimes say informally that an IO action "returns" something; however, taking that too literally leads to confusion. It is clear what we mean when we talk about functions returning results, but IO actions are not functions. Let's skip down to  — an IO action that does provide a value. is not a function that returns a  because   isn't a function. Rather,  is an IO action which, when evaluated, will materialize a , which can then be passed to later functions through, for instance,   and.

When we use  to get a , the value is monadic because it is wrapped in   functor (which happens to be a monad). We cannot pass the value directly to a function that takes plain (non-monadic, or non-functorial) values. does the work of taking a non-monadic function while passing in and returning monadic values.

As we've seen already,  does the work of passing a monadic value into a function that takes a non-monadic value and returns a monadic value. It may seem inefficient for  to take the non-monadic result of its given function and return a monadic value only for   to then pass the underlying non-monadic value to the next function. It is precisely this sort of chaining, however, that creates the reliable sequencing that make monads so effective at integrating pure functions with IO actions.

do notation review
Given the emphasis on sequencing, the  notation can be especially appealing with the   monad. Our program

could be written as:

The universe as part of our program
One way of viewing the  monad is to consider   as a computation which provides a value of type   while changing the state of the world by doing input and output. Obviously, you cannot literally set the state of the world; it is hidden from you, as the  functor is abstract (that is, you cannot dig into it to see the underlying values, a situation unlike what we have seen in the case of  ).

Understand that this idea of the universe as an object affected and affecting Haskell values through  is only a metaphor; a loose interpretation at best. The more mundane fact is that  simply brings some very base-level operations into the Haskell language. Remember that Haskell is an abstraction, and that Haskell programs must be compiled to machine code in order to actually run. The actual workings of IO happen at a lower level of abstraction, and are wired into the very definition of the Haskell language.

Pure and impure
The adjectives "pure" and "impure" often crop up while talking about I/O in Haskell. To clarify what they mean, we will revisit the discussion about referential transparency from the Prologue chapter. Consider the following snippet:

In most other programming languages, which do not have separate types for I/O actions,  would have a type akin to:

With such a type, however,  would not be a function at all! Functions produce the same results when given the same arguments; the  delivered by , however, also depends on whatever is typed at the terminal prompt. In Haskell, we avoid that pitfall by returning an, which is not a   but a promise that some   will be delivered by carrying out certain instructions involving I/O (in this case, the I/O consists of getting a line of input from the terminal). Though the  can be different each time   is evaluated, the I/O instructions are always the same.

When we say Haskell is a purely functional language, we mean that all of its functions are really functions – or, in other words, that Haskell expressions are always referentially transparent. If  had the problematic type we mentioned above, referential transparency would be violated:   would be a , and yet replacing it by any specific string would break the program.

In spite of Haskell being purely functional,  actions can be said to be impure because their impacts on the outside world are side effects (as opposed to the regular effects that are entirely contained within Haskell). Programming languages that lack purity may have side-effects in many other places connected with various calculations. Purely functional languages, however, assure that even expressions with impure values are referentially transparent. That means we can talk about, reason about and handle impurity in a purely functional way, using purely functional machinery such as functors and monads. While  actions are impure, all of the Haskell functions that manipulate them remain pure.

Functional purity, coupled with the fact that I/O shows up in types, benefits Haskell programmers in various ways. The guarantees about referential transparency increase a lot the potential for compiler optimizations. values being distinguishable through types alone make it possible to immediately tell where we are engaging with side effects or opaque values. As  itself is just another functor, we maintain to the fullest extent the predictability and ease of reasoning associated with pure functions.

Functional and imperative
When we introduced monads, we said that a monadic expression can be interpreted as a statement of an imperative language. That interpretation is immediately compelling for, as the language around IO actions looks a lot like a conventional imperative language. It must be clear, however, that we are talking about an interpretation. We are not saying that monads or  notation turn Haskell into an imperative language. The point is merely that you can view and understand monadic code in terms of imperative statements. The semantics may be imperative, but the implementation of monads and  is still purely functional. To make this distinction clear, let's look at a little illustration:

This is a snippet of C, a typical imperative language. In it, we declare a variable, read its value from user input with   and then print it with. We can, within an  do block, write a Haskell snippet that performs the same function and looks quite similar:

Semantically, the snippets are nearly equivalent. In the C code, however, the statements directly correspond to instructions to be carried out by the program. The Haskell snippet, on the other hand, is desugared to:

The desugared version has no statements, only functions being applied. We tell the program the order of the operations indirectly as a simple consequence of data dependencies: when we chain monadic computations with, we get the later results by applying functions to the results of the earlier ones. It just happens that, for instance, evaluating  leads to a string being printed in the terminal.

When using monads, Haskell allows us to write code with imperative semantics while keeping the advantages of functional programming.

I/O in the libraries
So far the only I/O primitives we have used were  and   and small variations thereof. The standard libraries, however, offer many other useful functions and actions involving. We present some of the most important ones in the IO chapter in Haskell in Practice, including the basic functionality needed for reading from and writing to files.

Monadic control structures
Given that monads allow us to express sequential execution of actions in a wholly general way, could we use them to implement common iterative patterns, such as loops? 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 presented here applied to, keep in mind that the following ideas apply to every monad.

Remember, there is nothing magical about monadic values; we can manipulate them just like any other values in Haskell. Knowing that, we might think to try the following function to get five lines of user input:

That won't do, however (try it in GHCi!). The problem is that  produces, in this case, a list of actions, while we want an action which returns a list (that is,   rather than  ). What we need is a fold to run 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:.

And so, we get the desired action with:

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

A particularly important combination is  and. Together, they allow us to make actions from a list of values, run them sequentially, and collect the results. , a Prelude function, captures this pattern:

We also have variants of the above functions with a trailing underscore in the name, such as,   and. These discard any final values and so are appropriate when you are only interested in performing actions. Compared with their underscore-less counterparts, these functions are like the distinction between  and. for instance has the following type:

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