Haskell/Prologue: IO, an applicative functor

The emergence of functors is a watershed in the course of this book. The reasons for that will begin to reveal themselves in this prologue, as we set the stage for the next several chapters of the book. While the code examples we will work with here are very simple, we will use them to bring several new and important ideas into play, ideas that will be revisited and further developed later in the book. That being so, we recommend that you study this chapter at a gentle pace, which will give you space for thinking about the implications of each step, as well as trying out the code samples in GHCi.

Scene 1 :
Our initial examples will use the function, which is provided by the   module.

provides a simple way of converting strings into Haskell values. If the provided string has the correct format to be read as a value of type,   gives back the converted value wrapped in  ; otherwise, the result is.

We can use  to write a little program in the style of those in the Simple input and output chapter that:


 * Gets a string given by the user through the command line;
 * Tries to read it into a number (let's use  as the type); and
 * If the read succeeds, prints your number times 2; otherwise, prints an explanatory message and starts over.

Here is a possible implementation:

GHCi> interactiveDoubling Choose a number: foo This is not a valid number. Retrying... Choose a number: 3 The double of your number is 6.0

Nice and simple. A variation of this solution might take advantage of how, given that  is a , we can double the value before unwrapping   in the case statement:

In this case, there is no real advantage in doing that. Still, keep this possibility in mind.

Application in functors
Now, let's do something slightly more sophisticated: reading two numbers with  and printing their sum (we suggest that you attempt writing this one as well before continuing).

Here is one solution:

works, but it is somewhat annoying to write. In particular, the nested  statements are not pretty, and make reading the code a little difficult. If only there was a way of summing the numbers before unwrapping them, analogously to what we did with  in the second version of , we would be able to get away with just one  :

But what should we put in place of ? , for one, is not enough. While  works just fine to partially apply   to the value wrapped by  ...

... we don't know how to apply a function wrapped in  to the second value. For that, we would need a function with a signature like this one...

... which would then be used like this:

The GHCi prompt in this example, however, is not wishful thinking:  actually exists, and if you try it in GHCi, it will actually work! The expression looks even neater if we use the infix synonym of,  :

The actual type  is more general than what we just wrote. Checking it...

... introduces us to a new type class:, the type class of applicative functors. For an initial explanation, we can say that an applicative functor is a functor which supports applying functions within the functor, thus allowing for smooth usage of partial application (and therefore functions of multiple arguments). All instances of  are  s, and besides , there are many other common  s which are also.

This is the  instance for  :

The definition of  is actually quite simple: if neither of the values are , apply the function   to   and wrap the result with  ; otherwise, give back. Note that the logic is exactly equivalent to what the nested  statement of   does.

Note that beyond  there is a second method in the instance above,  :

takes a value and brings it into the functor in a default, trivial way. In the case of, the trivial way amounts to wrapping the value with   – the nontrivial alternative would be discarding the value and giving back. With, we might rewrite the three-plus-four example above as...

... or even:

Just like the  class has laws which specify how sensible instances should behave, there is a set of laws for. Among other things, these laws specify what the "trivial" way of bringing values into the functor through  amounts to. Since there is a lot going on in this stretch of the book, we will not discuss the laws now; however, we will return to this important topic in a not too distant future.

To wrap things up, here is a version of  enhanced by  :

Scene 2 :
In the examples above, we have been taking I/O actions such as  for granted. We now find ourselves at an auspicious moment to revisit a question first raised many chapters ago: what is the type of ?

Back in the Simple input and output chapter, we saw the answer to that question is:

Using what we learned since then, we can now see that  is a type constructor with one type variable, which happens to be instantiated as   in the case of. That, however, doesn't get to the root of the issue: what does  really mean, and what is the difference between that and plain old  ?

Referential transparency
A key feature of Haskell is that all expressions we can write are referentially transparent. That means we can replace any expression whatsoever by its value without changing the behaviour of the program. For instance, consider this very simple program:

Its behaviour is wholly unsurprising:

Given that, we can rewrite   so that it doesn't mention. All we have to do is replace  with   on the right-hand side of the   definition and then replace  with the resulting expression. As advertised, the program behaviour does not change:

Referential transparency ensures that this sort of substitution works. This guarantee extends to anywhere in any Haskell program, which goes a long way towards making programs easier to understand, and their behaviour easier to predict.

Now, suppose that the type of  were. In that case, we would be able to use it as the argument to, as in:

In that case, however, a new question would spring forth: if  is a , which   is it? There is no satisfactory answer: it could be,  , or whatever else the user chooses to type at the terminal. And yet, replacing  with any   breaks the program, as the user would not be able to type the input string at the terminal any longer. Therefore  having type   would break referential transparency. The same goes for all other I/O actions: their results are opaque, in that it is impossible to know them in advance, as they depend on factors external to the program.

Cutting through the fog
As  illustrates, there is a fundamental indeterminacy associated with I/O actions. Respecting this indeterminacy is necessary for preserving referential transparency. In Haskell, that is achieved through the  type constructor. being an  means that it is not any actual , but both a placeholder for a   that will only materialise when the program is executed and a promise that this   will indeed be delivered (in the case of  , by slurping it from the terminal). As a consequence, when we manipulate an  we are setting up plans for what will be done once this unknown   comes into being. There are quite a few ways of achieving that. In this section, we will consider two of them; we will add a third one in the next few chapters.

The idea of dealing with a value which isn't really there might seem bizarre at first. However, we have already discussed at least one example of something not entirely unlike it without batting an eyelid. If  is a , then   doubles the value if it is there, and works regardless of whether the value actually exists. Both  and   imply, for different reasons, a layer of indirection in reaching the corresponding values of type. That being so, it comes as no surprise that, like,   is a  , with   being the most elementary way of getting across the indirection.

To begin with, we can exploit the fact that  is a   to replace the   definitions in   from the end of the previous section with something more compact:

can be read as "once  delivers a string, whatever it turns out to be, apply   to it". Referential transparency is not compromised: the value behind  is just as opaque as that of , and its type (in this case  ) prevents us from replacing it with any determinate value (say,  ) as it would violate referential transparency.

Beyond being a,   is also an  , which provides us with a second way of manipulating the values delivered by I/O actions. We will illustrate it with an  action, similar in spirit to. A first version is just below. Can you anticipate how to simplify it with ?

Here is a version exploiting :

is an I/O action which is made out of two other I/O actions (the two ). When it is executed, these two I/O actions are executed and the strings they deliver are concatenated. One important thing to notice is that  maintains a consistent order of execution between the actions it combines. Order of execution matters when dealing with I/O – examples of that are innumerable, but for starters consider this question: if we replace the second  in the example above with , which of the strings entered at the terminal will be cut down to three characters?

As  respects the order of actions, it provides a way of sequencing them. In particular, if we are only interested in sequencing and don't care about the result of the first action we can use  to discard it:

This is such a common usage pattern that there is an operator specifically for it:.

It can be readily applied to  example:

Or, going even further:

Note that each  replaces one of the magical line breaks of the   block that lead actions to be executed one after the other. In fact, that is all there is to the replaced line breaks: they are just syntactic sugar for.

Earlier, we said that a functor adds a layer of indirection for accessing the values within it. The flip side of that observation is that the indirection is caused by a context, within which the values are found. For, the indirection is that the values are only determined when the program is executed, and the context consists in the series of instructions that will be used to produce these values (in the case of  , these instructions amount to "slurp a line of text from the terminal"). From this perspective,  takes two functorial values and combines not only the values within but also the contexts themselves. In the case of  combining the contexts means appending the instructions of one I/O action to those of the other, thus sequencing the actions.

The end of the beginning
This chapter was a bit of a whirlwind! Let's recapitulate the key points we discussed in it:


 * is a subclass of  for applicative functors, which are functors that support function application without leaving the functor.
 * The  method of   can be used as a generalisation of   to multiple arguments.
 * An  is not a tangible value of type , but a placeholder for an   value that will only come into being when the program is executed and a promise that this value will be delivered through some means. That makes referential transparency possible even when dealing with I/O actions.
 * is a functor, and more specifically an instance of, that provides means to modify the value produced by an I/O action in spite of its indeterminacy.
 * A functorial value can be seen as being made of values in a context. The  operator (that is,  ) cuts through the context to modify the underlying values. The   operator combines both the contexts and the underlying values of two functorial values.
 * In the case of,  , and the closely related  , combine contexts by sequencing I/O actions.
 * A large part of the role of  blocks is simply providing syntactic sugar for.

As a final observation, note that there is still a major part of the mystery behind  blocks left to explain: what does the left arrow do? In a -block line such as...

... it looks like we are extracting the value produced by  from the   context. Thanks to the discussion about referential transparency, we now know that must be an illusion. But what is going on behind the scenes? Feel free to place your bets, as we are about to find out!