Functional Programming illustrated in Python: Part 5
The IO Monad — laid bare
From the Functional Programming illustrated in Python series
But I don’t like Monads!
Monads, Monads, Monads… have you got anything without Monads?
Well, there’s Direct Function Application. That doesn’t have much Monad in it.
The problem is, functions don’t do anything. Sooner or later you’re going to want to write a program which interacts with the real world. It reads and writes to the terminal. It writes to the filesystem. It updates a SQL database. It turns a little red LED on and off. All of these things are decidedly stateful and side-effect-ful, and they are implemented in dirty, impure languages like C and ultimately machine code. That code needs to be boxed up and presented in a functional way in order to interact safely with functional code. That box is conventionally¹ a Monad.
The pure ValueAndLog Monad we’ve been using all along captures the idea of “outputting”: it starts with an empty buffer, and the “side effect” of bind is to add strings together as it goes along².
Now imagine a similar class where instead of a functional side-effect, a real-world side-effect takes place, like writing to the terminal. That’s it. From the outside, its API might look like ValueAndLog. On the inside, instead of appending to a hidden buffer, it actually writes to the terminal. Functional world, meet real world.
You’ll also see another explanation:
- When performing impure operations, it’s important that they are performed in the right sequence. For instance, you need to insert a customer into your database before you can insert their first order.
- A pure functional language only evaluates things. Since pure functions depend only on their arguments, and not any external system state, they could be evaluated in different orders, or in parallel — or even not at all, if the result isn’t used anywhere.
- The Monad’s bind operation
a >> bcan be used to enforce ordering, in the same way that
h(g(f(v)))implies that you must evaluate f before g before h.
That is all true too. But the way I prefer to think of it is that in a purely functional Monad (like ValueAndLog), its bind operation reads and/or updates hidden state in the wrapper class. In a real-world Monad, its bind operation reads and/or updates state in the real world. That’s why a Monad is such a good container for stateful behaviour.
Warning: I am now going to unpick some Haskell code to expose the plumbing in Python. Any inaccuracies are entirely my fault. I welcome corrections from experts.
Let’s do it
Let’s wrap I/O so that it can be used by pure functional code. The end result should do the same as this imperative Python code:
print("What's your name?")
name = input()
print("Hello " + name)
The corresponding Haskell looks remarkably similar:
main = do
putStrLn "What's your name?"
name <- getLine
putStrLn ("Hello " ++ name)
But underneath it’s very different.
getLineis not a function! It’s a value from the IO class. This value causes the
bindoperation to read a line of text, and pass it to the function on the right-hand side of the
putStrLnis a function. But it doesn’t print a line of text! Rather, it returns a value from the IO class. That value tells the
bindoperation to print a particular line of text, and then call the function on the right-hand side.
Each of these IO values represents an “action”, something “to be done”, followed by doing “the next thing” (a.k.a. “the continuation”)
Nonetheless, this can still be translated into Python.
A class that represents Actions
Here is a naïve, but easy-to-understand, implementation of an IO action class.
def __init__(self, action, arg):
self.action = action
self.arg = arg def __rshift__(self, func): # this is "bind" (>>)
if self.action == "getLine":
line = input()
elif self.action == "putStrLn":
print(self.arg) # always returns None
elif self.action == "return":
raise RuntimeError("oops") @staticmethod
return IO("return", v)def putStrLn(text):
return IO("putStrLn", text)getLine = IO("getLine", None)
(Runnable code here)
The IO class contains a description of an action to be done, and the bind operation performs it. In each case, the value resulting from the action (if any) is given as the argument to the next function, and the return value of that function is returned, unchanged. The no-op action “return” just passes a wrapped value straight through.
That’s fine, although I don’t like that
getLine are global, so I am going to move them inside the IO class for tidiness:
return IO("putStrLn", text)IO.getLine = IO("getLine", None)
That’s a bit better. Remember that
getLine is not a function: it’s a constant value, an instance of the IO class, so it can’t be created until the class has been created.
putStrLn is a function which returns an IO value.
(Updated code here)
A better class
But there’s a more compact and natural way to do this. Each action can be represented as a function: an impure function, with no parameters, just like
say_hello from part 0 of this series. We can store the action function directly inside the IO wrapper. It boils down to just this:
def __init__(self, action):
self.action = action def __rshift__(self, func):
return func(self.action()) @staticmethod
return IO(lambda: v) @staticmethod
return IO(lambda: print(text))IO.getLine = IO(lambda: input())
Think about this carefully. Consider these examples:
v = IO.putStrLn("Hello") # what is the value of v ?
# does anything get printed yet? why?def dummy(x):
pass # do nothingv >> dummy # what does this do?IO.getLine # what is this value?
# does it read anything yet? why?IO.getLine >> dummy # what does this do?
If that’s not clear, look again at the one-line body of the bind operation:
def __rshift__(self, func):
self is the IO value on the left-hand side of
func is the function value on the right-hand side.
Step one is to pick out the action which this IO wrapper contains:
Step two is to execute it, which will do some action and give a result³:
Step three is to pass that value to the right-hand function:
And step four is to return the value returned by that function, to the caller of bind.
Therefore, when we create an instance of the IO class, we just need to provide a lambda which does the action we want to do, and returns a value to be passed on to the function on the right.
See how the ordering is enforced. The action must be executed before its result is passed to
func, since its return value forms the argument to
The main program
You can’t see any bind operations in the Haskell code, because they are hidden within the special syntax of the
To rewrite the
do block with its funny
<- as plain lambdas and binds, the lines are transformed one by one as described in the Assignments article. To recap:
do expr >> (lambda x:
x <- expr ⟾ do ...more code...
...more code... )
There is an extra case: you may have an expression whose unwrapped value is not used. Treat a bare
expr as if it were
ignore <- expr, unless it’s the last one.
do expr >> (lambda ignore:
expr ⟾ do ...more code...
...more code... )
The ignored parameter is conventionally named
_ (an underscore).
The result of these transformations is the following Python:
main = (
IO.putStrLn("What's your name?") >> (lambda _:
IO.getLine >> (lambda name:
IO.putStrLn("Hello " + name)
At each stage, the value is an IO “action”. When bound (
>>), the bind operator performs that action and passes its result (if any) as the argument to the function on the right-hand side (the continuation). That’s how
getLine >> (lambda name: ...) assigns the parameter
name to the result.
Finally, we need to run the program. What is
main anyway? Is it a function? No — for one thing, it doesn’t have any parameters, and a pure function with no parameters is a constant. It’s a value of some sort. It’s an IO action value: a chain of actions for the whole program.
To perform that action, we have to bind it — to a dummy function which does nothing and returns a dummy IO value⁵.
main >> (lambda _:
But in Python that explanation is not quite true. Python is an “eager” language, meaning it evaluates things as it goes along. In the process of calculating a value for
main, it performs the side effects of printing text and reading a line. The only action it doesn’t perform is the final one, which is the value assigned to
main. So by the time we have assigned a value to
main, it has done all the actions apart from the last one. The find bind does that.
In contrast, Haskell is a “lazy” language, which means it doesn’t evaluate expressions until it needs their value.
Anyway, here is the full code:
To make it more compact, you can rewrite those static method definitions as lambdas:
That is getting seriously terse. Welcome to functional programming.
I believe that’s an accurate translation of the Haskell shown earlier, but as I said before, I welcome corrections from those who know better.
Stripped bare like that though, it also shows there’s really nothing to it. The actions are still the same original impure actions,
print(...). The difference is that the result of each action is passed along by invoking the next function in the chain —the “continuation passing” style.
By the way, do you see how easy it is to add new actions? Try adding
IO.readFile, which takes a filename as its parameter and yields the contents of that file. In
main, you should then be able to replace
Unit in action
IO.unit function hasn’t served much purpose so far. To demonstrate it, I’m going to steal another example directly from the Haskell Wiki: the function
promptTwoLines asks for two pieces of information, and returns the concatenation of them.
promptLine prompt = do
getLinepromptTwoLines prompt1 prompt2 = do
line1 <- promptLine prompt1
line2 <- promptLine prompt2
return (line1 ++ " and " ++ line2)
main = do
both <- promptTwoLines "First line:" "Second line:"
putStrLn ("You said " ++ both)
Now a literal translation, where Haskell’s
return is our
Note the final expression in
IO.unit(line1 + " and " + line2)
This is the value which will be returned from
promptTwoLines. The final value of one of these chains of IO actions must be an IO action, but rather than actually perform any IO, in this case we just need to wrap a calculated value (as if we’d just received it using
getLine, say). Haskell’s
return means “prepare this value to be returned”. It’s not the same as Python’s
return, which is a flow-control construct (“stop executing this function right now”).
The value wrapped in IO is then unwrapped by bind at the point it is used, in this case as the parameter
both of the next lambda:
promptTwoLines(....) >> (lambda both:
putStrLn("You said " + both)
Is IO a Monad?
Or is it a burrito? You decide.
Actually, all you have to do is to check whether the IO class constructed above fulfils the Monad Laws given before. This is left as an exercise for the reader.
You can understand how this code works without knowing whether or not IO is a Monad. But if you wanted to pass the IO class to something else which works with Monads in general, then it would be important.
Acknowledgements and further reading
 Monads aren’t the only way to do I/O in functional languages.
 This might remind you of a
StringBuilder in some other languages, but note that ValueAndLog doesn’t mutate a buffer: its bind operation concatenates two strings together to make a new string value, and puts it inside a new ValueAndLog instance.
 The action function returns a plain, unwrapped value. It might be
None, but that’s still a value.
 In Haskell the bind operator, when it appears explicitly, is
>>=. I am using
>> because the code on the right is Python not Haskell, and Python doesn’t let me redefine the
>>= operator. This may be confusing, because Haskell has its own
>> operator which means something slightly different. Sorry, but I couldn’t see a way to avoid that.
 It returns
IO.unit(None) for correctness, because the function that bind invokes should always return a value of a wrapped type. But here this value is passed back through bind and discarded, and since Python isn’t type-safe it could be anything.