Thinking Functionally with Haskell (39 page)

Thus an action is a function that takes a world and delivers a value of type
a
and a new world. The new world is then used as the input for the next action. Having changed the world with an input–output action, you can’t go back to the old world. You can’t duplicate the world or inspect its components. All you can do is operate on the world with given primitive actions, and put such actions together in a sequence.

One primitive action is to print a character:

putChar :: Char -> IO ()

When executed, this action prints a character on the standard output channel, usually the computer screen. For example,
ghci> putChar 'x'

xghci>

The character
x
is printed, but nothing else, so the next GHCi prompt follows without additional spaces or newlines. Performing this action produces no value of interest, so the return value is the null tuple
()
.

Another primitive action is
done :: IO ()
, which does nothing. It leaves the world unchanged and also returns the null tuple
()
.

One simple operation to sequence actions is denoted by
(>>)
and has type
(>>) :: IO () -> IO () -> IO ()

Given actions
p
and
q
, the action
p >> q
first performs action
p
and then performs action
q
. For example,
ghci> putChar 'x' >> putChar '\n'

x

ghci>

This time a newline is printed. Using
(>>)
we can define the function
putStrLn
:

putStrLn :: String -> IO ()

putStrLn xs = foldr (>>) done (map putChar xs) >>

putChar '\n'

This action prints all the characters in a string, and then finishes up with an additional newline character. Note that
map putChar xs
is a list of actions. We are still in the universe of functional programming and its full expressive power, including uses of
map
and
foldr
, is still available to us.

Here is another primitive action:

getChar :: IO Char

When performed, this operation reads a character from the standard input channel. This channel is fed by you typing at the keyboard, so
getChar
returns the first character you type. For example,
ghci> getChar

x

'x'

After typing
getChar
and pressing return, GHCi waits for you to type a character. We typed the character
'x'
(and what we typed was echoed), and then that character was read and printed.

The generalisation of
done
is an action that does nothing and returns a named value:
return :: a -> IO a

In particular,
done = return ()
. The generalisation of
(>>)
has type
(>>) :: IO a -> IO b -> IO b

Given actions
p
and
q
, the action
p >> q
first does
p
, and then throws the return value away, and then does
q
. For example,
ghci> return 1 >> return 2

2

It is clear that this action is useful only when the value returned by
p
is not interesting since there is no way that
q
can depend on it. What is really wanted is a more general operator
(>>=)
with type
(>>=) :: IO a -> (a -> IO b) -> IO b

The combination
p >>= f
is an action that, when performed, first does
p
, returning a value
x
of type
a
, then does action
f x
returning a final value
y
of type
b
. It is easy to define
(>>)
in terms of
(>>=)
and we leave this as an exercise. The operator
(>>=)
is often referred to as
bind
, though one can also pronounce it as ‘then apply’.

Using
(>>=)
, we can define a function
getLine
for reading a line of input, more precisely, the list of characters up to but not including the first newline character:

getLine :: IO String

getLine = getChar >>= f

where f x = if x == '\n' then return []

else getLine >>= g

where g xs = return (x:xs)

This has a straightforward reading: get the first character
x
; stop if
x
is a newline and return the empty list; otherwise get the rest of the line and add
x
to the front. Though the reading is straightforward, the use of nested
where
clauses makes the
definition a little clumsy. One way to make the code smoother is to use anonymous lambda expressions and instead write:

getLine = getChar >>= \x ->

if x == '\n'

then return []

else getLine >>= \xs ->

return (x:xs)

Another, arguably superior solution is to use
do
-notation:

getLine = do x <- getChar

if x == '\n'

then return []

else do xs <- getLine

return (x:xs)

The right-hand side makes use of the Haskell layout convention. Note especially the indentation of the conditional expression, and the last
return
to show it is part of the inner
do
. Better in our opinion is to use braces and semicolons to control the layout explicitly:

getLine = do {x <- getChar;

if x == '\n'

then return []

else do {xs <- getLine;

return (x:xs)}}

We return to
do
-notation below.

The Haskell library
System.IO
provides many more actions than just
putChar
and
getChar
, including actions to open and read files, to write and close files, to buffer output in various ways and so on. We will not go into details in this book. But perhaps two more things need to be said. Firstly, there is no function of type
IO a -> a
1
. Once you are in a room performing input–output actions, you stay in the room and can’t come out of it. To see one reason this has to be the case, suppose there is such a function,
runIO
say, and consider

int :: Int

int = x - y

where x = runIO readInt

y = runIO readInt

readInt = do {xs <- getLine; return (read xs :: Int)}

The action
readInt
reads a line of input and, provided the line consists entirely of digits, interprets it as an integer. Now, what is the value of
int
? The answer depends entirely on which of
x
and
y
gets evaluated first. Haskell does not prescribe whether or not
x
is evaluated before
y
in the expression
x-y
. Put it this way: input– output actions have to be sequenced in a deterministic fashion, and Haskell is a lazy functional language in which it is difficult to determine the order in which things happen. Of course, an expression such as
x-y
is a very simple example (and exactly the same undesirable phenomenon arises in imperative languages) but you can imagine all sorts of confusion that would ensue if we were provided with
runIO
.

The second thing that perhaps should be said is in response to a reader who casts a lazy eye over an expression such as
undefined >> return 0 :: IO Int

Does this code raise an error or return zero? The answer is: an error. IO is
strict
in the sense that IO actions are performed in order, even though subsequent actions may take no heed of their results.

To return to the main theme, let us summarise. The type
IO a
is an abstract type on which the following operations, at least, are available:

return :: a -> IO a

(>>=) :: IO a -> (a -> IO b) -> IO b

 

putChar :: Char -> IO ()

getChar :: IO Char

The second two functions are specific to input and output, but the first two are not. Indeed they are general sequencing operations that characterise the class of types called
monads
:

class Monad m where

return :: a -> m a

(>>=) :: m a -> (a -> m b) -> m b

The two monad operations are required to satisfy certain laws, which we will come to in due course. As to the reason for the name ‘monad’, it is stolen from philosophy, in particular from Leibniz, who in turn borrowed it from Greek philosophy. Don’t read anything into the name.

10.2 More monads

If that’s all a monad is, then surely lots of things form a monad? Yes, indeed. In particular, the humble list type forms a monad:

instance Monad [] where

return x = [x]

xs >>= f = concat (map f xs)

Of course, we don’t yet know what the laws governing the monad operations are, so maybe this instance isn’t correct (it is), but at least the operations have the right types. Since
do
-notation can be used with any monad we can, for example, define the cartesian product function
cp :: [[a]] -> [[a]]
(see
Section 7.3
) using the new notation:

cp []
= return []

cp (xs:xss) = do {x <- xs;

ys <- cp xss;

return (x:ys)}

Comparing the right-hand side of the second clause to the list comprehension

[x:ys | x <- xs, ys <- cp xss]

one can appreciate that the two notations are very similar; the only real difference is that with
do
-notation the result appears at the end rather than at the beginning. If monads and
do
-notation had been made part of Haskell before list comprehensions, then maybe the latter wouldn’t have been needed.

Here is another example. The
Maybe
type is a monad:

instance Monad Maybe where

return x
= Just x

Nothing >>= f
= Nothing

Just x >>= f
= f x

To appreciate what this monad can bring to the table, consider the Haskell library function
lookup :: Eq a => a -> [(a,b)] -> Maybe b

The value of
lookup x alist
is
Just y
if
(x,y)
is the first pair in
alist
with first component
x
, and
Nothing
if there is no such pair. Imagine looking up
x
in
alist
, then looking up the result
y
in a second list
blist
, and then looking up the result
z
in yet a third list
clist
. If any of these lookups return
Nothing
, then
Nothing
is the final result. To define such a function we would have to write its defining expression as something like
case lookup x alist of

Nothing -> Nothing

Just y
-> case lookup y blist of

Nothing -> Nothing

Just z
-> lookup z clist

With a monad we can write

do {y <- lookup x alist;

z <- lookup y blist;

return (lookup z clist)}

Rather than having to write an explicit chain of computations, each of which may return
Nothing
, and explicitly passing
Nothing
back up the chain, we can write a simple monadic expression in which handling
Nothing
is done implicitly under a monadic hood.

do-notation

Just as list comprehensions can be translated into expressions involving
map
and
concat
, so
do
-expressions can be translated into expressions involving return and bind. The three main translation rules are:
do {p}
= p

do {p;stmts}
= p >> do {stmts}

do {x <- p;stmts} = p >> = \x -> do {stmts}

In these rules
p
denotes an action, so the first rule says that a
do
round a single action can be removed. In the second and third rules
stmts
is a
nonempty
sequence of statements, each of which is either an action or a statement of the form
x <- p
. The latter is
not
an action; consequently an expression such as
do {x <- getChar}

is not syntactically correct. Nor, by the way, is an empty
do
-expression
do { }
. The last statement in a
do
-expression must be an action.

On the other hand, the following two expressions are both fine:

do {putStrLn "hello "; name <- getLine; putStrLn name}

do {putStrLn "hello "; getLine; putStrLn "there"}

The first example prints a greeting, reads a name and completes the greeting. The second prints a greeting, reads a name but immediately forgets it, and then completes the greeting with a ‘there’. A bit like being introduced to someone in real life.

Other books

Self's punishment by Bernhard Schlink