Refactoring the Monadster

Dr Frankenfunctor and the Monadster, part 3

UPDATE: Slides and video from my talk on this topic

Warning! This post contains gruesome topics, strained analogies, discussion of monads

Welcome to the third installment in the gripping tale of Dr Frankenfunctor and the Monadster!

We saw in the first installment how Dr Frankenfunctor created life out of dead body parts using "Monadster part generators" (or "M"s for short), that would, on being supplied with some vital force, return a live body part.

We also saw how the leg and arms of the creature were created, and how these M-values could be processed and combined using mapM and map2M.

In the second installment we learned how the head, heart and body were built using other powerful techniques such as returnM, bindM and applyM.

In this last installment, we'll review all the techniques used, refactor the code, and compare Dr Frankenfunctor's techniques to the modern-day state monad.

Links for the complete series:

Review of the the techniques used

Before we refactor, let's review all the techniques we have used.

The M type

We couldn't create an actual live body part until the vital force was available, yet we wanted to manipulate them, combine them, etc. before the lightning struck. We did this by creating a type M that wrapped a "become alive" function for each part. We could then think of M<BodyPart> as a recipe or instructions for creating a BodyPart when the time came.

The definition of M was:

mapM

Next, we wanted to transform the contents of an M without using any vital force. In our specific case, we wanted to turn a broken arm recipe (M<BrokenLeftArm>) into a unbroken arm recipe (M<LeftArm>). The solution was to implement a function mapM that took a normal function 'a -> 'b and turned it into a M<'a> -> M<'b> function.

The signature of mapM was:

map2M

We also wanted to combine two M-recipes to make a new one. In that particular case, it was combining an upper arm (M<UpperRightArm>) and lower arm (M<LowerRightArm>) into a whole arm (M<RightArm>). The solution was map2M.

The signature of map2M was:

returnM

Another challenge was to lift a normal value into the world of M-recipes directly, without using any vital force. In that particular case, it was turning a Skull into an M<Skull> so it could be used with map2M to make a whole head.

The signature of returnM was:

Monadic functions

We created many functions that had a similar shape. They all take something as input and return a M-recipe as output. In other words, they have this signature:

Here are some examples of actual monadic functions that we used:

bindM

The functions up to now did not require access to the vital force. But then we found that we needed to chain two monadic functions together. In particular, we needed to chain the output of makeLiveHeart (with signature DeadHeart -> M<LiveHeart>) to the input of makeBeatingHeart (with signature LiveHeart -> M<BeatingHeart>). The solution was bindM, which transforms monadic functions of the form 'a -> M<'b> into functions in the M-world (M<'a> -> M<'b>) which could then be composed together.

The signature of bindM was:

applyM

Finally, we needed a way to combine a large number of M-parameters to make the live body. Rather than having to create special versions of map (map4M, map5M, map6M, etc), we implemented a generic applyM function that could apply an M-function to an M-parameter. From that, we could work with a function of any size, step by step, using partial application to apply one M-parameter at a time.

The signature of applyM was:

Defining the others functions in terms of bind and return

Note that of all these functions, only bindM needed access to the vital force.

In fact, as we'll see below, the functions mapM, map2M, and applyM can actually be defined in terms of bindM and returnM!

Refactoring to a computation expression

A lot of the functions we have created have a very similar shape, resulting in a lot of duplication. Here's one example:

In particular, there is a lot of explicit handling of the vital force.

In most functional languages, there is a way to hide this so that the code looks much cleaner.

In Haskell, developers use "do-notation", in Scala people use "for-yield" (the "for comprehension"). And in F#, people use computation expressions.

To create a computation expression in F#, you just need two things to start with, a "bind" and a "return", both of which we have.

Next, you define a class with specially named methods Bind and Return:

And finally, you create an instance of this class:

When this is done, we have access to the special syntax monster {...}, just like async{...}, seq{...}, etc.

  • The let! x = xM syntax requires that the right side is an M-type, say M<X>.

    let! unwraps the M<X> into an X and binds it to the left side -- "x" in this case.

  • The return y syntax requires that return value is a "normal" type, Y say.

    return wraps it into a M<Y> (using returnM) and returns it as the overall value of the monster expression.

So some example code would look like this:

If you want more on computation expressions, I have an in-depth series of posts about them.

Redefining mapM and friends

With monster expressions available, let's rewrite mapM and the other functions.

mapM

mapM takes a function and a wrapped M-value and returns the function applied to the inner value.

Here is an implementation using monster:

If we compile this implementation, we get the same signature as the previous implementation:

map2M

map2M takes a function and two wrapped M-values and returns the function applied to both the values.

It's also easy to write using monster expressions:

If we compile this implementation, we again get the same signature as the previous implementation:

applyM

applyM takes a wrapped function and a wrapped value and returns the function applied to the value.

Again, it's trivial to write using monster expressions:

And the signature is as expected

Manipulating the vital force from within a monster context

We'd like to use the monster expression to rewrite all our other functions too, but there is a stumbling block.

Many of our functions have a body that looks like this:

In other words, we're getting some of the vital force and then putting a new vital force to be used for the next step.

We are familiar with "getters" and "setters" in object-oriented programming, so let's see if we can write similar ones that will work in the monster context.

Introducing getM

Let's start with the getter. How should we implement it?

Well, the vital force is only available in the context of becoming alive, so the function must follow the familiar template:

Note that getting the vitalForce doesn't use any up, so the original amount can be returned untouched.

But what should happen in the middle? And what should be returned as the first element of the tuple?

The answer is simple: just return the vital force itself!

getM is a M<VitalForce> value, which means that we can unwrap it inside a monster expression like this:

Introducing putM

For the putter, the implementation is a function with a parameter for the new vital force.

Again, what should we do in the middle?

The most important thing is that the newVitalForce becomes the value that is passed on to the next step. We must throw away the original vital force!

Which in turn means that newVitalForce must be used as the second part of the tuple that is returned.

And what should be in the first part of the tuple that is returned? There is no sensible value to return, so we'll just use unit.

Here's the final implementation:

With getM and putM in place, we can now create a function that

  • gets the current vital force from the context

  • extracts one unit from that

  • replaces the current vital force with the remaining vital force

  • returns the one unit of vital force to the caller

And here's the code:

Using the monster expression to rewrite all the other functions

With useUpOneUnitM, we can start to rewrite all the other functions.

For example, the original function makeLiveLeftLegM looked like this, with lots of explicit handling of the vital force.

The new version, using a monster expression, has implicit handling of the vital force, and consequently looks much cleaner.

Similarly, we can rewrite all the arm surgery code like this:

And so on. This new code is much cleaner.

In fact, we can make it cleaner yet by eliminating the intermediate values such as armSurgery and armSurgeryM and putting everything in the one monster expression.

We can use this approach for the head as well. We don't need headSurgery or returnM any more.

Finally, we can use a monster expression to create the whole body too:

NOTE: The complete code using monster expressions is available on GitHub.

Monster expressions vs applyM

We previously used an alternative way to create the body, using applyM.

For reference, here's that way using applyM:

So what's the difference?

Aesthetically there is a bit of difference, but you could legitimately prefer either.

However, there is a much more important difference between the applyM approach and the monster expression approach.

The applyM approach allows the parameters to be run independently or in parallel, while the monster expression approach requires that the parameters are run in sequence, with the output of one being fed into the input of the next.

That's not relevant for this scenario, but can be important for other situations such as validation or async. For example, in a validation context, you may want to collect all the validation errors at once, rather than only returning the first one that fails.

Relationship to the state monad

Dr Frankenfunctor was a pioneer in her time, blazing a new trail, but she did not generalize her discoveries to other domains.

Nowadays, this pattern of threading some information through a series of functions is very common, and we give it a standard name: "State Monad".

Now to be a true monad, there are various properties that must be satisfied (the so-called monad laws), but I am not going to discuss them here, as this post is not intended to be a monad tutorial.

Instead, I'll just focus on how the state monad might be defined and used in practice.

First off, to be truly reusable, we need to replace the VitalForce type with other types. So our function-wrapping type (call it S) must have two type parameters, one for the type of the state, and one for the type of the value.

With this defined, we can create the usual suspects: runS, returnS and bindS.

Personlly, I'm glad that we got to understood how these worked in the M context before making them completely generic. I don't know about you, but signatures like these

would be really hard to understand on their own, without any preparation.

Anyway, with those basics in place we can create a state expression.

getS and putS are defined in a similar way as getM and putM for monster.

Property based testing of the state expression

Before moving on, how do we know that our state implementation is correct? What does it even mean to be correct?

Well, rather than writing lots of example based tests, this is a great candidate for a property-based testing approach.

The properties we might expect to be satisfied include:

  • Only the last put counts. That is, putting X then putting Y should be the same as just putting Y.

  • Get should return the last put. That is, putting X then doing a get should return the same X.

and so on.

I won't go into this any more right now. I suggest watching the talk for a more in-depth discussion.

Using the state expression instead of the monster expression

We can now use state expressions exactly as we did monster expressions. Here's an example:

Another example is how to build a BeatingHeart:

As you can see, the state expression automatically picked up that VitalForce was being used as the state -- we didn't need to specify it explicitly.

So, if you have a state expression type available to you, you don't need to create your own expressions like monster at all!

For a more detailed and complex example of the state monad in F#, check out the FSharpx library.

NOTE: The complete code using state expressions is available on GitHub.

Other examples of using a state expression

The state computation expression, once defined, can be used for all sorts of things. For example, we can use state to model a stack.

Let's start by defining a Stack type and associated functions:

Note that none of that code knows about or uses the state computation expression.

To make it work with state, we need to define a customized getter and putter for use in a state context:

With these in place we can start coding our domain!

Stack-based Hello World

Here's a simple one. We push "world", then "hello", then pop the stack and combine the results.

Stack-based calculator

Here's a simple stack-based calculator:

And now we can combine these basic states to build more complex ones:

Remember that, just as with the vital force, all we have now is a recipe for building a stack. We still need to run it to execute the recipe and get the result.

Let's add a helper to run all the operations and return the top of the stack:

Now we can evaluate the operations, like this:

OK, OK, some monad stuff

People always want to know about monads, even though I do not want these posts to degenerate into yet another monad tutorial.

So here's how they fit in with what we have worked with in these posts.

A functor (in a programming sense, anyway) is a data structure (such as Option, or List, or State) which has a map function associated with it. And the map function has some properties that it must satisfy (the "functor laws").

A applicative functor (in a programming sense) is a data structure (such as Option, or List, or State) which has two functions associated with it: apply and pure (which is the same as return). And these functions have some properties that they must satisfy (the "applicative functor laws").

Finally, a monad (in a programming sense) is a data structure (such as Option, or List, or State) which has two functions associated with it: bind (often written as >>=) and return. And again, these functions have some properties that they must satisfy (the "monad laws").

Of these three, the monad is most "powerful" in a sense, because the bind function allows you to chain M-producing functions together, and as we have seen, map and apply can be written in terms of bind and return.

So you can see that both our original M type and the more generic State type, in conjunction with their supporting functions, are monads, (assuming that our bind and return implementations satisfy the monad laws).

For a visual version of these definitions, there is a great post called Functors, Applicatives, And Monads In Pictures.

Further reading

There are a lot of posts about state monads on the web, of course. Most of them are Haskell oriented, but I hope that those explanations will make more sense after reading this series of posts, so I'm only going to mention a few follow up links.

And for another important use of "bind", you might find my talk on functional error handling useful.

If you want to see F# implementations of other monads, look no further than the FSharpx project.

Summary

Dr Frankenfunctor was a groundbreaking experimentalist, and I'm glad that I have been able to share insights on her way of working.

We've seen how she discovered a primitive monad-like type, M<BodyPart>, and how mapM, map2M, returnM, bindM and applyM were all developed to solve specific problems.

We've also seen how the need to solve the same problems led to the modern-day state monad and computation expression.

Anyway, I hope that this series of posts has been enlightening. My not-so-secret wish is that monads and their associated combinators will no longer be so shocking to you now...

shocking

...and that you can use them wisely in your own projects. Good luck!

NOTE: The code samples used in this post are available on GitHub.

Last updated

Was this helpful?