Haskell/Prologue: IO, an applicative functor
|For shorter links to this chapter, be them within the book or off-wiki, you can use the Haskell/Applicative prologue redirect.|
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 you to study this chapter at a gentle pace, which gives 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
readMaybe, which is provided by the
GHCi> :m +Text.Read GHCi> :t readMaybe readMaybe :: Read a => String -> Maybe a
readMaybe 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
readMaybe gives back the converted value wrapped in
Just; otherwise, the result is
GHCi> readMaybe "3" :: Maybe Integer Just 3 GHCi> readMaybe "foo" :: Maybe Integer Nothing GHCi> readMaybe "3.5" :: Maybe Integer Nothing GHCi> readMaybe "3.5" :: Maybe Double Just 3.5
We can use
readMaybe 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
Doubleas the type); and
- If the read succeeds, prints the double of the number; otherwise, prints an explanatory message and starts over.
Here is a possible implementation:
import Text.Read interactiveDoubling = do putStrLn "Choose a number:" s <- getLine let mx = readMaybe s :: Maybe Double case mx of Just x -> putStrLn ("The double of your number is " ++ show (2*x)) Nothing -> do putStrLn "This is not a valid number. Retrying..." interactiveDoubling
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
Maybe is a
Functor, we can double the value before unwrapping
mx in the case statement:
interactiveDoubling = do putStrLn "Choose a number:" s <- getLine let mx = readMaybe s :: Maybe Double case fmap (2*) mx of Just d -> putStrLn ("The double of your number is " ++ show d) Nothing -> do putStrLn "This is not a valid number. Retrying..." interactiveDoubling
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
readMaybe and printing their sum (we suggest that you attempt writing this one as well before continuing).
Here is one solution:
interactiveSumming = do putStrLn "Choose two numbers:" sx <- getLine sy <- getLine let mx = readMaybe sx :: Maybe Double my = readMaybe sy case mx of Just x -> case my of Just y -> putStrLn ("The sum of your numbers is " ++ show (x+y)) Nothing -> retry Nothing -> retry where retry = do putStrLn "Invalid number. Retrying..." interactiveSumming
GHCi> interactiveSumming Choose two numbers: foo 4 Invalid number. Retrying... Choose two numbers: 3 foo Invalid number. Retrying... Choose two numbers: 3 4 The sum of your numbers is 7.0
interactiveSumming works, but it is somewhat annoying to write. In particular, the nested
case 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
fmap in the second version of
interactiveDoubling, we would be able to get away with just one
-- Wishful thinking... case somehowSumMaybes mx my of Just z -> putStrLn ("The sum of your numbers is " ++ show z) Nothing -> do putStrLn "Invalid number. Retrying..." interactiveSumming
But what should we put in place of
fmap, for one, is not enough. While
fmap (+) works just fine to partially apply
(+) to the value wrapped by
GHCi> :t (+) 3 (+) 3 :: Num a => a -> a GHCi> :t fmap (+) (Just 3) fmap (+) (Just 3) :: Num a => Maybe (a -> a)
... we don't know how to apply a function wrapped in
Maybe to the second value. For that, we would need a function with a signature like this one...
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
... which would then be used like this:
GHCi> fmap (+) (Just 3) <*> Just 4 Just 7
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
GHCi> (+) <$> Just 3 <*> Just 4 Just 7
The actual type
(<*>) is more general than what we just wrote. Checking it...
GHCi> :t (<*>) (<*>) :: Applicative f => f (a -> b) -> f a -> f b
... introduces us to a new type class:
Applicative, 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
Functors, and besides
Maybe, there are many other common
Functors which are also
This is the
Applicative instance for
instance Applicative Maybe where pure = Just (Just f) <*> (Just x) = Just (f x) _ <*> _ = Nothing
The definition of
(<*>) is actually quite simple: if neither of the values are
Nothing, apply the function
x and wrap the result with
Just; otherwise, give back
Nothing. Note that the logic is exactly equivalent to what the nested
case statement of
Note that beyond
(<*>) there is a second method in the instance above,
GHCi> :t pure pure :: Applicative f => a -> f a
pure takes a value and brings it into the functor in a default, trivial way. In the case of
Maybe, the trivial way amounts to wrapping the value with
Just – the nontrivial alternative would be discarding the value and giving back
pure, we might rewrite the three-plus-four example above as...
GHCi> (+) <$> pure 3 <*> pure 4 :: Num a => Maybe a Just 7
... or even:
GHCi> pure (+) <*> pure 3 <*> pure 4 :: Num a => Maybe a Just 7
Just like the
Functor class has laws which specify how sensible instance should behave, there is a set of laws for
Applicative. Among other things, these laws specify what the "trivial" way of bringing values into the functor through
pure 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
interactiveSumming enhanced by
interactiveSumming = do putStrLn "Choose two numbers:" sx <- getLine sy <- getLine let mx = readMaybe sx :: Maybe Double my = readMaybe sy case (+) <$> mx <*> my of Just z -> putStrLn ("The sum of your numbers is " ++ show z) Nothing -> do putStrLn "Invalid number. Retrying..." interactiveSumming
Scene 2 :
In the examples above, we have been taking I/O actions such as
getLine 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:
GHCi> :t getLine getLine :: IO String
Using what we learned since then, we can now see that
IO is a type constructor with one type variable, which happens to be instantiated as
String in the case of
getLine. That, however, doesn't get to the root of the issue: what does
IO String really mean, and what is the difference between that and plain old
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:
addExclamation :: String -> String addExclamation s = s ++ "!" main = putStrLn (addExclamation "Hello")
Its behaviour is wholly unsurprising:
GHCi> main Hello!
addExclamation s = s ++ "!", we can rewrite
main so that it doesn't mention
addExclamation. All we have to do is replacing
"Hello" in the right-hand side of the
addExclamation definition and then replacing
addExclamation "Hello!" by the resulting expression. As advertised, the program behaviour does not change:
GHCi> let main = putStrLn ("Hello" ++ "!") GHCi> main Hello!
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
String. In that case, we would be able to use it as the argument to
addExclamation, as in:
-- Not actual code. main = putStrLn (addExclamation getLine)
In that case, however, a new question would spring forth: if
getLine is a
String is it? There is no satisfactory answer: it could be
"Goodbye", or whatever else the user chooses to type at the terminal. And yet, replacing
getLine by any
String breaks the program, as the user would not be able to type the input string at the terminal any longer. Therefore
getLine having type
String would cause referential transparency to be broken. The same goes for all other I/O actions: their results are opaque, in that it is impossible to tell them in advance, as they depend on factors external to the program.
Cutting through the fog
getLine 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
IO type constructor.
getLine being an
IO String means that it is not any actual
String, but both a placeholder for a
String that will only materialise when the program is executed and a promise that this
String will indeed be delivered (in the case of
getLine, by slurping it from the terminal). As a consequence, when we manipulate an
IO String we are setting up plans for what will be done once this unknown
String comes into being. There are quite a few ways of achieving that. In this section, we will consider two of them; to which we will add a third one in the next few chapters.
The idea of dealing 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
mx is a
Maybe Double, then
fmap (2*) mx doubles the value if it is there, and works regardless of whether the value actually exists. Both
Maybe a and
IO a imply, for different reasons, a layer of indirection in reaching the corresponding values of type
a. That being so, it comes as no surprise that, like
IO is a
fmap being the most elementary way of getting across the indirection.
To begin with, we can exploit the fact of
IO being a
Functor to replace the
let definitions in
interactiveSumming from the end of the previous section by something more compact:
interactiveSumming :: IO () interactiveSumming = do putStrLn "Choose two numbers:" mx <- readMaybe <$> getLine -- equivalently: fmap readMaybe getLine my <- readMaybe <$> getLine case (+) <$> mx <*> my :: Maybe Double of Just z -> putStrLn ("The sum of your numbers is " ++ show z) Nothing -> do putStrLn "Invalid number. Retrying..." interactiveSumming
readMaybe <$> getLine can be read as "once
getLine delivers a string, whatever it turns out to be, apply
readMaybe on it". Referential transparency is not compromised: the value behind
readMaybe <$> getLine is just as opaque as that of
getLine, and its type (in this case
IO (Maybe Double)) disallows us from replacing it with any determinate value (say,
Just 3) that would violate referential transparency.
Beyond being a
IO is also an
Applicative, which provides us a second way of manipulating the values delivered by I/O actions. We will illustrate it with a
interactiveConcatenating action, similar in spirit to
interactiveSumming. A first version is just below. Can you anticipate how to simplify it with
interactiveConcatenating :: IO () interactiveConcatenating = do putStrLn "Choose two strings:" sx <- getLine sy <- getLine putStrLn "Let's concatenate them:" putStrLn (sx ++ sy)
Here is a version exploiting
interactiveConcatenating :: IO () interactiveConcatenating = do putStrLn "Choose two strings:" sz <- (++) <$> getLine <*> getLine putStrLn "Let's concatenate them:" putStrLn sz
(++) <$> getLine <*> getLine is an I/O action which is made out of two other I/O actions (the two
getLine). 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
getLine in the example above with
(take 3 <$> getLine), which of the strings entered at the terminal will be cut down to three characters?
(<*>) 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
\_ y -> y to discard it:
GHCi> (\_ y -> y) <$> putStrLn "First!" <*> putStrLn "Second!" First! Second!
This is such a common usage pattern that there is an operator specifically for it:
u *> v = (\_ y -> y) <$> u <*> v
GHCi> :t (*>) (*>) :: Applicative f => f a -> f b -> f b GHCi> putStrLn "First!" *> putStrLn "Second!" First! Second!
It can be readily applied to
interactiveConcatenating :: IO () interactiveConcatenating = do putStrLn "Choose two strings:" sz <- (++) <$> getLine <*> getLine putStrLn "Let's concatenate them:" *> putStrLn sz
Or, going even further:
interactiveConcatenating :: IO () interactiveConcatenating = do sz <- putStrLn "Choose two strings:" *> ((++) <$> getLine <*> getLine) putStrLn "Let's concatenate them:" *> putStrLn sz
Note that each of the
(*>) replaces one of the magical line breaks of the
do 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 brings in 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
IO, 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
getLine, 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
IO 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:
Applicativeis a subclass of
Functorfor applicative functors, which are functors that support function application without leaving the functor.
Applicativecan be used as a generalisation of
fmapto multiple arguments.
IO ais not a tangible value of type
a, but a placeholder for an
avalue 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.
IOis a functor, and more specifically an instance of
Applicative, 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.
fmapcuts through the context to modify the underlying values.
(<*>)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
doblocks is simply providing syntactic sugar for
As a final observation, note that there is still a major part of the mystery behind
do blocks left to explain: what does the left arrow do? In a
do-block line such as...
sx <- getLine
... it looks like we are extracting the value produced by
getLine from the
IO 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!
- The key difference between the two situations is that with
Maybethe indeterminacy is only apparent, and it is possible to figure out in advance whether there is an actual
mx– or, more precisely, it is possible as long as the value of
mxdoes not depend on I/O!