Chapter 08: Monadic Onions

Pointy Functor Factory

If you recall, IO and Task's constructors expect a function as their argument, but Maybe and Either do not. The motivation for this interface is a common, consistent way to place a value into our functor. The term "default minimal context" lacks precision, yet captures the idea well: we'd like to lift any value in our type and map away per usual with the expected behaviour of whichever functor.

One important correction I must make at this point, pun intended, is that Left doesn't make any sense. Each functor must have one way to place a value inside it and with Either, that's Right(x). We define Right because if our type can map, it should map.

You may have heard of functions such as pure, point, unit, and return. These are various monikers will become important when we start using monads because, as we will see, it's our responsibility to place values back into the type manually.

Mixing Metaphors

onion

You see, in addition to space burritos (if you've heard the rumors), monads are like onions. Allow me to demonstrate with a common situation:

#read_file :: str -> IO str
read_file = lambda filename: IO(lambda: open(filename,'r').read())
def print_and_pass(x):
print(x)
return x
# io_print :: str -> IO str
io_print = lambda x: IO(lambda: print_and_pass(x))
# cat :: str -> IO (IO str)
cat = compose(map(io_print), read_file)
cat('../.git/config')
# IO(IO('[core]\nrepositoryformatversion = 0\n'))

What we've got here is an IO trapped inside another IO because io_print introduced a second IO during our map. To continue working with our string, we must map(map(f)) and to observe the effect, we must unsafe_perform_IO().unsafe_perform_IO().

# cat_first_char :: str -> IO (IO str)
cat_first_char = compose(map(map(head)), cat)
cat_first_char('../.git/config')
# IO(IO('['))

While it is nice to see that we have two effects packaged up and ready to go in our application, it feels a bit like working in two hazmat suits and we end up with an uncomfortably awkward API. Let's look at another situation:

# safe_value :: key -> {key: a} -> Maybe a
safe_value = curry(lambda k, d: Maybe(d.get(k, Nothing)))
# safe_head :: [a] -> Maybe a
safe_head = safe_value('0')
# first_address_street :: User -> Maybe (Maybe (Maybe Street))
first_address_street = compose(
map(map(safe_value('street'))),
map(safe_head),
safe_value('addresses'))
first_address_street({
'addresses': {'0': { 'street': { 'name': 'Mulburry', 'number': 8402 }, 'postcode': 'WC2N' }}
})
# Maybe(Maybe(Maybe({'name': 'Mulburry', 'number': 8402})))

Again, we see this nested functor situation where it's neat to see there are three possible failures in our function, but it's a little presumptuous to expect a caller to map three times to get at the value - we'd only just met. This pattern will arise time and time again and it is the primary situation where we'll need to shine the mighty monad symbol into the night sky.

I said monads are like onions because tears well up as we peel back each layer of the nested functor with map to get at the inner value. We can dry our eyes, take a deep breath, and use a method called join.

mm = Maybe(Maybe('nunchucks'))
# Maybe(Maybe('nunchucks'))
mm.join()
## Maybe('nunchucks')
ioio = IO(IO('pizza'))
# IO(IO('pizza'))
ioio.join()
# IO('pizza')
ttt = Task(Taskof(Task('sewers')))
# Task(Task(Task('sewers')))
ttt.join()
# Task(Task('sewers'))

If we have two layers of the same type, we can smash them together with join. This ability to join together, this functor matrimony, is what makes a monad a monad. Let's inch toward the full definition with something a little more accurate:

Monads are pointed functors that can flatten

Any functor which defines a join method and obeys a few laws is a monad. Defining join is not too difficult so let's do so for Maybe:

class Maybe:
def join(self):
if self.is_nothing():
return Maybe(Nothing())
return self.value

There, simple as consuming one's twin in the womb. If we have a Maybe(Maybe(x)) then .$value will just remove the unnecessary extra layer and we can safely map from there. Otherwise, we'll just have the one Maybe as nothing would have been mapped in the first place.

Now that we have a join method, let's sprinkle some magic monad dust over the first_address_street example and see it in action:

# join :: Monad m => m (m a) -> m a
join = lambda mma: mma.join()
# first_address_street :: User -> Maybe (Maybe (Maybe Street))
first_address_street = compose(
join,
map(safe_value('street')),
join,
map(safe_head),
safe_value('addresses'))
first_address_street({
'addresses': {'0': { 'street': { 'name': 'Mulburry', 'number': 8402 }, 'postcode': 'WC2N' }}
})
# Maybe({'name': 'Mulburry', 'number': 8402})

We added join wherever we encountered the nested Maybe's to keep them from getting out of hand. Let's do the same with IO to give us a feel for that.

class IO:
...
def join(self):
return self.unsafe_peform_IO()

Again, we simply remove one layer. Mind you, we have not thrown out purity, but merely removed one layer of excess shrink wrap.

My Chain Hits My Chest

chain

You might have noticed a pattern. We often end up calling join right after a map. Let's abstract this into a function called chain.

# chain :: Monad m => (a -> m b) -> m a -> m b
chain = curry(lambda f, m: m.map(f).join())
# or
# chain :: Monad m => (a -> m b) -> m a -> m b
chain = lambda f: compose(join, map(f))

We'll just bundle up this map/join combo into a single function. If you've read about monads previously, you might have seen chain called >>= (pronounced bind) or flat_map which are all aliases for the same concept. I personally think flat_map is the most accurate name, but we'll stick with chain as it's the widely accepted name. Let's refactor the two examples above with chain:

# map/join
first_address_street = compose(
join,
map(safe_value('street')),
join,
map(safe_head),
safe_value('addresses'))
# chain
first_address_street = compose(
chain(safe_value('street')),
chain(safe_head),
safe_value('addresses'),
);

I swapped out any map/join with our new chain function to tidy things up a bit. Cleanliness is nice and all, but there's more to chain than meets the eye - it's more of a tornado than a vacuum. Because chain effortlessly nests effects, we can capture both sequence and variable assignment in a purely functional way.

// getJSON :: Url -> Params -> Task JSON
getJSON('/authenticate', { username: 'stale', password: 'crackers' })
.chain(user => getJSON('/friends', { user_id: user.id }));
// Task([{name: 'Seimith', id: 14}, {name: 'Ric', id: 39}]);
// querySelector :: Selector -> IO DOM
querySelector('input.username')
.chain(({ value: uname }) =>
querySelector('input.email')
.chain(({ value: email }) => IO.of(`Welcome ${uname} prepare for spam at ${email}`))
);
// IO('Welcome Olivia prepare for spam at olivia@tremorcontrol.net');
Maybe(3).chain(lambda three: Maybe(2).map(add(three)))
# Maybe(5)
Maybe(Nothing()) \
.chain(safe_value('address')) \
.chain(safe_value('street'))
# Maybe(Nothing());

We could have written these examples with compose, but we'd need a few helper functions and this style lends itself to explicit variable assignment via closure anyhow. Instead we're using the infix version of chain which, incidentally, can be derived from map and join for any type automatically: def chain(self, fn): return self.map(fn).join(). We can also define chain manually if we'd like a false sense of performance, though we must take care to maintain the correct functionality - that is, it must equal map followed by join. An interesting fact is that we can derive map for free if we've created chain simply by bottling the value back up when we're finished. With chain, we can also define join as chain(id). It may feel like playing Texas Hold em' with a rhinestone magician in that I'm just pulling things out of my behind, but, as with most mathematics, all of these principled constructs are interrelated. Lots of these derivations are mentioned in the fantasyland repo.

Anyways, we have two examples using Maybe. Since chain is mapping under the hood, if any value is null, we stop the computation dead in its tracks.

Don't worry if these examples are hard to grasp at first. Play with them. Poke them with a stick. Smash them to bits and reassemble. Remember to map when returning a "normal" value and chain when we're returning another functor. In the next chapter, we'll approach Applicatives and see nice tricks to make this kind of expressions nicer and highly readable.

As a reminder, this does not work with two different nested types. Functor composition and later, monad transformers, can help us in that situation.

Power Trip

Container style programming can be confusing at times. We sometimes find ourselves struggling to understand how many containers deep a value is or if we need map or chain (soon we'll see more container methods). We can greatly improve debugging with tricks like implementing inspect and we'll learn how to create a "stack" that can handle whatever effects we throw at it, but there are times when we question if it's worth the hassle.

I'd like to swing the fiery monadic sword for a moment to exhibit the power of programming this way.

Let's read a file, then upload it directly afterward:

# read_file :: os.path -> Either str (Task ValueError str)
# http_post :: str -> str -> Task RuntimeError JSON
# upload :: str -> Either str (Task Exception JSON)
upload = compose(map(chain(http_post('/uploads'))), read_file);

Here, we are branching our code several times. Looking at the type signatures I can see that we protect against 3 errors - read_file uses Either to validate the input (perhaps ensuring the filename is present), read_file may error when accessing the file as expressed in the first type parameter of Task, and the upload may fail for whatever reason which is expressed by the Error in http_post. We casually pull off two nested, sequential asynchronous actions with chain.

All of this is achieved in one linear left to right flow. This is all pure and declarative. It holds equational reasoning and reliable properties. We aren't forced to add needless and confusing variable names. Our upload function is written against generic interfaces and not specific one-off apis. It's one bloody line for goodness sake.

For contrast, let's look at the standard imperative way to pull this off:

// upload :: str -> (str -> a) -> None
def upload(filename, callback):
if not filename:
raise ValueError('You need a filename!')
with open(filenema, 'r') as f:
reponse = requests.post('/uploads, f.read())
if reponse.status_code != 200:
raise RuntimeError('Failed to upload data)
callback(response.json)

Well isn't that the devil's arithmetic. We're pinballed through a volatile maze of madness. Imagine if it were a typical app that also mutated variables along the way! We'd be in the tar pit indeed.

Theory

The first law we'll look at is associativity, but perhaps not in the way you're used to it.

# associativity
compose(join, map(join)) == compose(join, join)

These laws get at the nested nature of monads so associativity focuses on joining the inner or outer types first to achieve the same result. A picture might be more instructive:

monad associativity law

Starting with the top left moving downward, we can join the outer two M's of M(M(M a)) first then cruise over to our desired M a with another join. Alternatively, we can pop the hood and flatten the inner two M's with map(join). We end up with the same M a regardless of if we join the inner or outer M's first and that's what associativity is all about. It's worth noting that map(join) != join. The intermediate steps can vary in value, but the end result of the last join will be the same.

The second law is similar:

// identity for all (M a)
compose(join, M) === compose(join, map(M)) === id;

It states that, for any monad M and join amounts to id. We can also map(M) and attack it from the inside out. We call this "triangle identity" because it makes such a shape when visualized:

monad identity law

If we start at the top left heading right, we can see that M does indeed drop our M a in another M container. Then if we move downward and join it, we get the same as if we just called id in the first place. Moving right to left, we see that if we sneak under the covers with map and call of of the plain a, we'll still end up with M (M a) and joining will bring us back to square one.

I should mention that I've just written of, however, it must be the specific M for whatever monad we're using.

Now, I've seen these laws, identity and associativity, somewhere before... Hold on, I'm thinking...Yes of course! They are the laws for a category. But that would mean we need a composition function to complete the definition. Behold:

mcompose = lambda f, g: compose(chain(f), g)
# left identity
mcompose(M, f) == f
# right identity
mcompose(f, M) == f
# associativity
mcompose(mcompose(f, g), h) == mcompose(f, mcompose(g, h));

They are the category laws after all. Monads form a category called the "Kleisli category" where all objects are monads and morphisms are chained functions. I don't mean to taunt you with bits and bobs of category theory without much explanation of how the jigsaw fits together. The intention is to scratch the surface enough to show the relevance and spark some interest while focusing on the practical properties we can use each day.

In Summary

Monads let us drill downward into nested computations. We can assign variables, run sequential effects, perform asynchronous tasks, all without laying one brick in a pyramid of doom. They come to the rescue when a value finds itself jailed in multiple layers of the same type. With the help of the trusty sidekick "pointed", monads are able to lend us an unboxed value and know we'll be able to place it back in when we're done.

Yes, monads are very powerful, yet we still find ourselves needing some extra container functions. For instance, what if we wanted to run a list of api calls at once, then gather the results? We can accomplish this task with monads, but we'd have to wait for each one to finish before calling the next. What about combining several validations? We'd like to continue validating to gather the list of errors, but monads would stop the show after the first Left entered the picture.

In the next chapter, we'll see how applicative functors fit into the container world and why we prefer them to monads in many cases.