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 >> b can 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.

  • getLine is not a function! It’s a value from the IO class. This value causes the bind operation to read a line of text, and pass it to the function on the right-hand side of the bind.
  • putStrLn is a function. But it doesn’t print a line of text! Rather, it returns a value from the IO class. That value tells the bind operation 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.

class IO:
def __init__(self, action, arg):
self.action = action
self.arg = arg
def __rshift__(self, func): # this is "bind" (>>)
if self.action == "getLine":
line = input()
return func(line)
elif self.action == "putStrLn":
print(self.arg) # always returns None
return func(None)
elif self.action == "return":
return func(self.arg)
raise RuntimeError("oops")
def unit(v):
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 putStrLn and getLine are global, so I am going to move them inside the IO class for tidiness:

class IO:
def putStrLn(text):
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:

class IO:
def __init__(self, action):
self.action = action
def __rshift__(self, func):
return func(self.action())
def unit(v):
return IO(lambda: v)
def putStrLn(text):
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 nothing
v >> 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):
return func(self.action())

self is the IO value on the left-hand side of >>, and func is the function value on the right-hand side.

Step one is to pick out the action which this IO wrapper contains:

return func(self.action())

Step two is to execute it, which will do some action and give a result³:

return func(self.action())

Step three is to pass that value to the right-hand function:

return func(self.action())
^^^^^ ^

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 func.

The main program

You can’t see any bind operations in the Haskell code, because they are hidden within the special syntax of the do block⁴.

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, input() and 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 IO.getLine with IO.readFile("/etc/hostname").

Unit in action

The 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
putStrLn prompt
promptTwoLines 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 IO.unit:

Note the final expression in promptTwoLines:

        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

[1] Monads aren’t the only way to do I/O in functional languages.

[2] 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.

[3] The action function returns a plain, unwrapped value. It might be None, but that’s still a value.

[4] In Haskell the bind operator, when it appears explicitly, is >>=

[5] It returns IO.unit(None) for correctness, because the function that bind invokes should always return a wrapped value. But here this value is passed back through bind and discarded, and since Python isn’t type-safe it could be anything.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store