-
Notifications
You must be signed in to change notification settings - Fork 49
Three Useful Monads
This is a translation of Three Useful Monads from Haskell into Python. Hopefully this should make the article much easier to understand for people who don't know Haskell. All code samples uses the Python OSlash library (Python 3 only).
How to make the examples work with Python:
$ pip3 install oslash
python3
>>> from oslash import *
Note: before reading this, you should know what a monad is. Read this post if you don’t!
Here’s a function half
:
In Python:
>>> half = lambda x: x//2
And we can apply it a couple of times:
>>> half(half(8))
2
Or we can compose the functions together to a new function:
>>> from oslash.util import compose
>>> quarter = compose(half, half)
>>> quarter(8)
2
Everything works as expected. Now you decide that you want to log what happens in this function:
>>> half = lambda x: (x//2, "I just halved %d!" % x)
Okay, fine. Now what if you want to apply half a couple of times?
Spoilers: it doesn’t happen automatically. You have to do it yourself:
>>> final_value = (
... lambda v1, l1: (
... lambda v2, l2: (v2, l1+l2))(*half(v1))
... )(*half(8))
Yuck! That’s nowhere as nice as:
>>> half(half(8))
And what if you have more functions that log things? There’s a pattern here: for each function that returns a log along with a value, we want to combine those logs. This is a side-effect, and monads are great at side effects!
Writer monad arriving on a horse
The Writer monad is cool. “Hey dude, I’ll handle the logging,” says Writer. “Go back to your clean code and crank up some Zeppelin!” Every writer has a log and a return value:
class Writer(Monad, Functor):
"""The writer monad."""
...
Writer lets us write code like this:
>> half(8) | half
Or you can use the Monad compose
function, which does function composition with monads, to get:
>>> quarter = Monad.compose(half, half)
>>> quarter(8)
2 :: I just halved 8!I just halved 4!
which is pretty darn close to compose(half, half)
that we used before. Cool!
You use tell
to write something to the log. And unit
puts a value in a Writer. Here’s our new half
function:
>>> unit, tell = Writer.unit, MonadWriter.tell
>>>
>>> half = lambda x: tell("I just halved %s!" % x).bind(lambda _: unit(x//2))
It returns a Writer
:
And we can use the run
method to extract the values from the Writer
:
>>> half(8).run()
(4, 'I just halved 8!')
But the cool part is, now we can chain calls to half with |
:
>>> quarter = half(8) | half
>>> quarter.run()
(2, 'I just halved 8!I just halved 4!')
Here’s what’s happening:
|
(>>= in Haskell) magically knows how to combine two writers, so we don’t have to write any of that tedious code ourselves! Here’s the full definition of |
:
Which is the same boilerplate code we had written before. Except now, |
takes care of it for us. Cool! We also used unit
('return' in Haskell), which takes a value and puts it in a monad:
Writer.unit(val) = Writer(val, "")
(Note: these definitions are almost right. The real Writer
monad allows us to use any Monoid
as the log, not just strings. I have simplified it here a bit).
Thanks, Writer monad!
Suppose you want to pass some config around to a lot of functions. Use the Reader
monad:
The reader monad lets you pass a value to all your functions behind the scenes. For example:
unit = Reader.unit
ask = MonadReader.ask
greeter = ask() | (lambda name: unit("Hello, %s!" % name))
greeter
returns a Reader
monad:
Here’s how Reader is defined:
class Reader(Monad, Applicative, Functor):
...
Reader was always the renegade. The wild card. Reader is different because it’s only field is a function, and this is confusing to look at. But we both understand that you can use run
(runReader
in Haskell) to get that function:
And then you give this function some state, and it’s used in greeter
:
>>> greeter.run()("adit")
'Hello, adit!'
So when you use |
(>>=
in Haskell), you should get a Reader
back. When you pass in a state to that reader, it should be passed through to every function in that monad.
m | k = Reader(lambda r: (k(m.run(r))).run(r))
Reader
always was a little complex. The complex ones are the best.
unit
(return
in Haskell) puts a value in a Reader
:
unit(value) = Reader(lambda _: value)
And finally, ask
gives you back the state that was passed in:
ask() = Reader(lambda x: x)
Want to spend some more time with Reader? Turn up the punk rock and see this longer example.
The State monad is the Reader monad’s more impressionable best friend:
She’s exactly like the Reader monad, except you can write as well as read!
Here’s how State
is defined:
class State(Monad, Functor):
...
You can get the state with get
, and change it with put
. Here’s an example:
>>> def greeter():
... state = get().bind(lambda name:
... put("tintin").bind(
... lambda _: unit("hello, %s!" % name)))
... return state
...
>>> greeter().run("adit")
(hello, adit!, "tintin")
Nice! Reader was all like “you won’t change me”, but State is committed to this relationship and willing to change.
The definitions for the State
monad look pretty similar to the definitions for the Reader
monad:
unit
:
@classmethod
def unit(cls, value: Any) -> 'State':
return cls(lambda state: (value, state))
bind
aka |
(>>=
in Haskell):
def bind(self, fn: Callable[[Any], 'State']) -> 'State':
def _(result, state):
return fn(result).run(state)
return State(lambda state: _(*self.run(state)))
Writer. Reader. State. You added three powerful weapons to your Haskell arsenal today. Use them wisely.