Hopefully in my last post I convinced that you monads could be a useful common pattern for programming languages to support.
While doing that though I was very vague about how it would actually be implemented, to the point where my code examples were pretty vague, and wouldn't actually work if you thought about it.
I'd like to try and clear that up a bit by showing you how a language could add features to support this common pattern that we have identified. To do that we'll just use our design skills to build something that works, and not deal with any of the type theory that can be used to derive these ideas.
Injecting control flow
My illustration of how the feature could work hinged on
the magic <<==
operator. We said that it would change
what value the rest of the code would see. It also needed
to change behaviour depending on which of our categories
of behaviour we wanted.
Remember that our four categories were:
- Optional/nullable values
- Errors/exceptions
- Lists/arrays
- Promises
If we need to run different code depending on the type of the value, then let's try adding a method to the value that has the behaviour that we want.
Let's start with promises. We want the behaviour to
be that the rest of the code receives the value when
it is ready. This is the then
method that promises
already expose:
p.then(rest);
will call the rest of the code with the value when it is available.
How can we apply this to the other categories. For
lists we said that we wanted to run the rest of the
code for each of the values in the list. This is the
map
method in those languages that have it.
l.map(rest)
How about the other categories. In each case it's hard to define the method, because we either have the value, or we have something else, namely null, or an exception bubbling up the stack.
Let's proceed by just defining an interface for this, and then worry about making it work with the rest of the language later. For a nullable value, so let's define a new class that holds the value or null, and we can define the behaviour there.
class Nullable {
constructor(value) {
this.value = value;
}
bind(rest) {
if (this.value === null) {
return null;
}
return rest(this.value);
}
}
We called the method bind
as that is the convention
for monads, but it could be any name really.
Now let's do the same thing for errors, but this time we need a couple of different implementations.
class MaybeError {
}
class IsError extends MaybeError {
constructor(message) {
this.message = message;
}
bind(rest) {
// There was an error, so propogate it,
// don't peform the rest of the computations.
return this;
}
}
class NotError extends MaybeError {
constructor(value) {
this.value = value;
}
bind(rest) {
return rest(this.value);
}
}
(Again this is very weird code and you likely wouldn't write it this way naturally, especially with those names, but hopefully you can see that it is valid code.)
Now for each of our types we can call a method on the value
passing the rest of the code, and it will call it in the right
way for that type. We can alias the then
method from promises
and the map
method from lists to bind
so that we have one
method that we can call regardless of the type.
We can now start chaining the operations using our bind
function,
for example:
getArticleID().bind(function(articleID) {
return getArticleContent(articleID);
});
If getArticleID()
returns something with a bind method then it will
be called with the rest of the code that we want to execute. The value
can then decide how to execute that code, whether getArticleID
returns
a nullable value, an error, a list, or a promise, what you get out
as the result depends on the particular types that are used.
This code may look pretty familiar, particularly if you have worked with promises. There's good reason to structure code like this, even if you don't have any language features that support monads. In fact you can implement all of this in pretty much every language, so again, why all the fuss about monads?
Removing the magic from <<==
Again it comes back to the magic <<==
operator. Now that we
have defined everything above we can finally define how it would work,
and remove all the magic.
We want to take the code from above
getArticleID().bind(function(articleID) {
return getArticleContent(articleID);
});
and rewrite it to be more compact and less nested.
articleID <<== getArticleID();
return getArticleContent(articleID);
Our language then takes this and transforms it in to the function
calls from before. <<==
is just "sugar" that makes the code nicer
to write, but underneath it is just tranformed in to function calls
using our bind
method.
So now we see how <<==
works we can see that it is pretty powerful.
We can define a simple bind
method, and then our languages special
<<==
syntax allows us to use that method to change how the code
flow happens, but without resorting to lots of nested functions.
The great thing is that it's one bit of syntax that doesn't care about
any of the specifics, it can work for anything: lists, promises, errors,
or anyting the user defines that we haven't thought of yet.
Filling in some gaps
Again I've glossed over some details which might be confusing.
The first is that in order to get a fully working system the bind
method isn't quite sufficient. You need a second method for each
type, but it's even easier than bind
. This method takes a plain
value and turns it in to one of the special ones. This is
usually called return
but that's confusing in most languages,
as it doesn't mean "return this value from this function", so
let's call it wrap
.
To define wrap you just have to take a plain value and turn it in
to one of your type in the most "plain" way possible. There is an
exact mathematical definition for that, but it usually just means
the easy, obvious way to do it. For promises we again have the
method already defined: resolve
. This takes the value and
fires the callbacks with it straightaway. It allows you to
take a value you have now, and pretend that it's a value that
you don't know yet.
(In JS the resolve
method doesn't follow the mathematical
definition required for a monad because of the behaviour
when you give it another promise. However we would fix that
if we were actually using a language with this special support)
For a list, it's pretty obvious how to turn a plain value in to
a list. We wrap it in square brackets: [value]
.
For the other two we need to use those special classes that
we defined before. For a Nullable
we would just use the
constructor: new Nullable(value)
. For a MaybeError
we would
use the NotError
constructor: new NotError(value)
.
The other gap to fill in is how weird the Nullable
and MaybeError
classes are. They aren't things that you would usually write, because
there other ways of handling them in most languages.
You have three options if you are building a language with special monad handling:
- Have nulls and exceptions, and
<<==
doesn't support them. - Have nulls and exceptions, and have the language make them work with
<<==
with some special handling (e.g. null definesbind
) - Don't have traditional nulls and exceptions, and have
Nullable
andMaybeError
instead. This is what e.g. Haskell does. There are some advantages of this as well as natural<<==
support, such as great synergy with static type checking.
Summary
We define bind
and wrap
methods for each monad type, and that
provides a consistent interface to chain operations that allow the
value to dictate how the operations link together. We can then
define an operator like <<==
that the language translates
to callbacks using bind
.
Hopefully this has cleared up any confusion that the vague code samples in my last article caused. I hope that it might also have deepened your understanding of monads, and maybe helped you see how they can be useful.