Fantas, Eel, and Specification 15: Monad
05 Jun 2017Today is the day, Fantasists. We all knew it was coming, but we hoped it wouldn’t be so soon. Sure enough, though, here we are. We’ve battled through weeks of structures, and reached the dreaded Monad
. Say your goodbyes to your loved ones, and let’s go.
Ahem.
A Monad
is a type that is both a Chain
and an Applicative
. That’s… well, it, really. We’re done here. Next time, we’ll be looking at the new-wave wizardry of Extend
. Until then, take care!
♥
… Right, so maybe we could say a little more, but only if we want to! Honestly, though, the above is enough to get going. We’ve seen a few examples of Semigroup
-and-Monoid
-feeling relationships, but let’s focus on two in particular:
In the Apply
post, we said that ap
felt a bit Semigroup
-ish. Then, in the Applicative
post, we saw that adding of
gave us something Monoid
-ish.
Later, we looked at Chain
, where chain
gave us something like a Semigroup
. So, you’re asking, where’s the Monoid
? Well, with a Monad
, of
doubles up as the empty
to Chain
’s Semigroup
, too!
We’re going to go through a pretty mathematical definition of Monad
first, so don’t be discouraged if it doesn’t make sense on the first few reads. This is really just background knowledge for the curious; skip ahead if you just want to see a practical example!
To skip ahead, start scrolling!
Let’s do some mind-blowing; it’s the Monad
post after all, right? Let’s first define two composition functions, compose
and mcompose
:
//- Regular `compose` - old news!
//+ compose :: (b -> c)
//+ -> (a -> b)
//+ -> a -> c
const compose = f => g => x =>
f(g(x))
//- `chain`-sequencing `compose`, fancily
//- known as Kleisli composition - it's the
//- K in Ramda's "composeK"!
//+ mcompose :: Chain m
//+ => (b -> m c)
//+ -> (a -> m b)
//+ -> a -> m c
const mcompose = f => g => x =>
g(x).chain(f)
I’ve tried to line up the types so it’s a bit clearer to see left-to-right how this works… I hope that it helped in some way!
compose
says, “Do g
, then f
”. mcompose
says the same thing, but does it with some kind of context (little language extension bubble, remember?). That m
could be Maybe
in the case of two functions that may fail, or Array
in the case of two functions that return multiple values, and so on. What’s important is that, to use mcompose
, our m
must be a Chain
type.
Now, you can make something very monoid-looking with regular compose
:
const Compose = daggy.tagged('Compose', ['f'])
//- Remember, for semigroups:
//- concat :: Semigroup s => s -> s -> s
//- Replace s with (a -> a)...
//+ concat :: (a -> a)
//+ -> (a -> a)
//+ -> a -> a
Compose.prototype.concat =
function (that) {
return Compose(
x => this(that(x))
)
}
//- We need something that has no effect...
//- The `id` function!
//+ empty :: (a -> a)
Compose.empty = () => Compose(x => x)
Mind blown yet? Function composition is a monoid! The x => x
function is our empty
(because it doesn’t do anything), and composition is concat
(because it combines two functions into a pipeline). See? Everything is just monoids. Monoids all the way down.
Typically, the
Compose
type is used for other things (remember theTraversable
post?), but we’re using it here as just a nice, clear name for this example.
Now, here’s the real wizardry: can we do the same thing with mcompose
? Well, we could certainly write a Semigroup
:
const MCompose = daggy.tagged('MCompose', ['f'])
//- Just as we did with Compose...
//+ concat :: Chain m
//+ => (a -> m a)
//+ -> (a -> m a)
//+ -> a -> m a
MCompose.prototype.concat =
function (that) {
return MCompose(
x => that(x).chain(this)
)
}
concat
now just does mcompose
instead of compose
, as we expected. If we want an empty
, though, it would need to be an a -> m a
function. Well, reader mine, it just so happens that we’ve already seen that very function: from Applicative
, the of
function!
//- So, we need empty :: (a -> m a)
//+ empty :: Chain m, Applicative m
//+ => (a -> m a)
MCompose.empty = () =>
MCompose(x => M.of(x))
// Or just `MCompose(M.of)`!
Note that, as with lots of interesting
Monoid
types, we’d need aTypeRep
to buildMCompose
to know whichM
type we’re using.
To make MCompose
a full Monoid
, we need our M
type to have an of
method and be Chain
able. Chain
for the Semigroup
, plus Applicative
for the Monoid
.
Take a breath, Fantasists: I’m aware that I might be alone here, but I think this is beautiful. No matter how clever we think we’re being, it’s all really just Semigroup
s and Monoid
s at the end of the day. Under the surface, it never gets more complex than that.
Let’s not get too excited just yet, though; remember that there are laws with empty
. Think back to the Monoid
post: it has to satisfy left and right identity.
// For any monoid x...
x
// Right identity
=== x.concat(M.empty())
// Left identity
=== M.empty().concat(x)
// So, for `MCompose` and some `f`...
MCompose(f)
// Right identity
=== MCompose(f).concat(MCompose.empty())
// Left identity
=== MCompose.empty().concat(MCompose(f))
//- In other words, `of` can't disrupt the
//- sequence held inside `mcompose`! For
//- the sake of clarity, this just means:
f.chain(M.of).chain(g) === f.chain(g)
f.chain(g).chain(M.of) === f.chain(g)
And there we have it: of
cannot disrupt the sequence. All it can do is put a value into an empty context, placing it somewhere in our sequence. No tricks, no magic, no side-effects.
So, for your most strict and correct definition, M
is a Monad
if you can substitute it into our MCompose
without breaking the Monoid
laws. That’s it!
For those skipping ahead, stop scrolling!
Ok, big deal, Monad
is to Chain
as Monoid
is to Semigroup
; why is everyone getting so excited about this, though? Well, remember how we said we could use Chain
to define execution order?
//+ getUserByName :: String -> Promise User
const getUserByName = name =>
new Promise(res => /* Some AJAX */)
//+ getFriends :: User -> Promise [User]
const getFriends = user =>
new Promise(res => /* Some more AJAX */)
// e.g. returns [every, person, ever]
getUser('Baymax').chain(getFriends)
With this, we can define entire programs using map
and chain
! We can do this because we can sequence our actions. What we get with of
is the ability to lift variables into that context whenever we like!
const optimisedGetFriends = user
user.name == "Howard Moon"
? Promise.of([]) // Lift into Promise
: getFriends(user) // Promise-returner
We know that getFriends
returns a Promise
, so our speedy result needs to do the same. Luckily, we can just lift our speedy result into a pure Promise
, and we’re good to go!
Although it may seem improbable, we actually now have the capability to write any IO
logic we might want to write:
const Promise = require('fantasy-promises')
const rl =
require('readline').createInterface({
input: process.stdin,
output: process.stdout
})
//+ prompt :: Promise String
const prompt = new Promise(
res => rl.question('>', res))
//- We use "Unit" to mean "undefined".
//+ speak :: String -> Promise Unit
const speak = string => new Promise(
res => res(console.log(string)))
//- Our entire asynchronous app!
//+ MyApp :: Promise String
const MyApp =
// Get the name...
speak('What is your name?')
.chain(_ => prompt)
.chain(name =>
// Get the age...
speak('And what is your age?')
.chain(_ => prompt)
.chain(age =>
// Do the logic...
age > 30
? speak('Seriously, ' + name + '?!')
.chain(_ => speak(
'You don\'t look a day over '
+ (age - 10) + '!'))
: speak('Hmm, I can believe that!'))
// Return the name!
.chain(_ => Promise.of(name)))
//- Our one little impurity:
// We run our program with a final
// handler for when we're all done!
MyApp.fork(name => {
// Do some database stuff...
// Do some beeping and booping...
console.log('FLATTERED ' + name)
rl.close() // Or whatever
})
That, beautiful Fantasists, is (basically) an entirely purely-functional app. Let’s talk about a few cool things here.
Firstly, every step is chain
ed together, so we’re explicitly giving the order in which stuff should happen.
Secondly, we can nest chain
to get access to previous values in later actions.
Thirdly, chain
means we can do everything with arrow functions. Every command is a single-expression function; it’s super neat! Try re-formatting this example on a bigger screen; all my examples are written for mobile, but this example can look far more readable with 80-character width!
Fourthly, following on from the first point, there’s no mention of async with chain
- we specify the order, and Promise.chain
does the promise-wiring for us! At this point, async behaviour is literally just an implementation detail.
Fifthly (are these still words?), MyApp
- our whole program - is a value! It has a type Promise String
, and we can use that String
! What does that mean? We can chain programs together!
//+ BigApp :: Promise Unit
const BigApp =
speak('PLAYER ONE')
.chain(_ => MyApp)
.chain(player1 =>
speak('PLAYER TWO')
.chain(_ => MyApp)
.chain(player2 =>
speak(player1 + ' vs ' + player2)))
OMGWTF! We took our entire program and used it as a value… twice! As a consequence, we can just write lots of little programs and chain (compose, concat, bind, whatever you want to say) them together into bigger ones! Remember, too, that Monad
s are all also Applicatives
…
//+ BigApp_ :: Promise Unit
const BigApp_ =
lift2(x => y => x + ' vs ' + y,
speak('PLAYER ONE').chain(_ => MyApp),
speak('PLAYER TWO').chain(_ => MyApp))
Oh yeah! Our programs are now totally composable Applicative
s, just like any other value. Our entire programs! All a functional program really does is collect some little programs together with ap
and chain
. It really is that neat!
Why do we use
fantasy-promises
instead of the built-inPromise
? Our functionalPromise
doesn’t execute until we callfork
- that means we can delay the call until we’ve defined its behaviour. With a built-inPromise
, things start happening immediately, which can lead to non-determinism and race conditions. This way, we maintain full control!
Of course, maybe the syntax is a bit ugly, but that’s what helper functions are for! Also, why stop at Promise
? This fanciness works for Maybe
, Array
, Either
, Function
, Pair
, and so many more!
Keep fiddling, using those Task
/Promise
isomorphisms to do things in parallel, using Maybe
to avoid undefined
/ null
along the way, using Array
to return multiple choices; if you can handle all that, you’re a fully-fledged functional aficionado!
You might be wondering what the rest is for if we now have all the tools we’ll ever need, and that’s certainly a good question. The rest are optional; monadic functional programming doesn’t require an understanding of Comonad
or Profunctor
, but nor does it require an understanding of Alt
or Traversable
; these are just design patterns to help our code to be as polymorphic as possible.
As always, there’s a Gist for the article, so have a play with it! Here’s a little idea for an exercise: write a monadic functional CLI app to play “higher or lower”. You know everything you need to know; trust me!
♥