Tracing computation expressions

F# has a novel syntax feature called computation expressions, which lets you build complex monadic expressions with minimal syntax. Commonly shied away from, a monad is simple: it’s a function whose input is some state. A monad usually manipulates the state and returns a new state (which can be handed off to another monad).

Monad’s are maybe best known from Haskell, but they exist in scheme, ML, clojure, scala, and even show up in C# and other imperative languages.

While the computation expression syntax is cool, short of the maybe monad, and the baked-in async keyword, I wasn’t sure what I could do with this. Thankfully, I found an interesting post by Luis Diego Fallas who posted an F# code sample leveraging computation expressions to read binary formatted files. However, if you are like me, and trying to better understand the power of computation expressions, tracing through these samples can be difficult. It’s not because they are poorly written, but mostly because they are jumping into more complex usages.

To clarify what’s going on with computation expressions, I wanted to show what is passing through the monad. Computation expressions (also known as monads or workflows) can be tricky because you are overriding the language syntax. So what is a “let!” statement in one workflow is not the same as in another workflow. On top of that statements themselves can return a value, not just have a left hand side assignment. If you feel your brain start to hurt, that’s OK. It will get better.

An example

Let’s start with a small sample to see how information passes through the workflow. This makes it easier to understand what computation expressions are doing:

Desugaring

Before we trace through what’s happening, lets desugar the expression. If you aren’t familiar with the bang syntax, a let! statement will compile into an execution on the builders Bind function and a return will compile into an execution on the builders Return function. Let’s take the original expression:

let build _ = builder{
let! x = 1
let! y = 2
return 3
}

And show the same thing but without the syntactic sugar.

let desugared =
builder.Bind(1, fun next ->
let x = next // next = 1 since we took the input value of the bind,
// and passed it to the next lambda
let returnedState =
builder.Bind(2, fun next2 ->
let y = next2 // next2 is the value 2 here
let terminatedState = builder.Return(3)
//terminated state is now
//State.Current(3, State.Terminated)
terminatedState
)
// the state here is
// State.Current(2, State.Current(3, State.Terminated))
returnedState
)

The important thing to understand here is how the let! statement is deconstructed. The right hand side is the value input to the bind. Everything below the let! statement (and including the left hand side of the assignment), is inside of the bind lambda (passed to the rest argument of the bind function). The first thing the lambda does is actually apply the left hand side assignment with the input from the bind. In this case, the first let! statement assigns x = 1. Then it executes the remaining function, being the other let! and return statements.

Tracing it through

In the original sample, I’ve defined a discriminated union called State representing the current state. This union has two types. One, called Current, contains an integer as well as a link to the next state in the form of a tuple. The other, Terminated, is a value that we can use as a terminator for our state link. The example is really just for demonstration, since by being able to capture the state it’s easier to understand how computation expressions work; it gives us a sense of where the monad has been.

Let’s take this one step at a time, with an even simpler example based on the above code. It’s important to understand the deconstruction. The compiler will translate our computation expressions into invocations on the builder.

To desguar it we take the left hand side and everything after

And move it to a lambda. This lambda is what is going to be passed as the rest argument to the builder’s bind function

The right hand side is going to be applied to the value argument of the bind function

Combine and Yield

Computation expressions have more than just let! and return statements though. Once you get used to tracing through and thinking about the computation builder, it becomes easier to start writing workflows. Just for kicks, I wanted to see if I could write a computation expression to evaluate a basic arithmetic expression. Here I’m using partial functions and the Combine property of the builder to build out the expression. If you wanted to, you can even use computation expressions within computation expressions. There’s nothing keeping you from doing that.

In general, there is a bunch of reserved syntax that maps to specific builder functions. The msdn on computation syntax has these all defined.

Each monadic function is wrapped in a Delay, which promptly executes it. Look at the builder’s delay declaration – it takes a function and executes it.

In our builder, the Yield just returns the same value. It doesn’t do much but we needed to implement it to use the computation expression syntax.

What we pass to the delay is an anonymous function that has a combine statement. Combines take two things and produce a third. Here, we are passing the current partial function as the first argument (via the yield), and the value we want to use to evaluate this partial function as the second argument. However, the second argument isn’t actually evaluated till the end. The combine will then apply the second argument (an integer) to the first argument (a partial function that takes an integer).

For the basic arithmetic example, the final delay function returns 0. You can think of this as a “seed.” If you think of it like a fold operation, this is very similar. When we finally return the seed, we bubble each evaluated expression back up the stack (starting with 0), so read the desugared version from the bottom up. In this way, we are executing the current curried statement with the previous statements evaluated value in the Combine method of the builder. Not the most practical application, but I thought it was a fun exercise.

If you are confused why this example’s desguaring contains the Delay method and the original example didn’t, it’s because the sugaring happens differently depending which builder constructs you use.

Under the hood

When you decompile a computation expression, each monadic function gets compiled into it’s own class representing a monad. In our arithmetic operation example, this is a Combine, Yield, Delay trio. It’s not easy to read since the function names have been mangled, but you can see the general pattern here (formatting added).

Notice in the decompiled block that the class name is runu004089 and the executable expression is compiled into an Invoke that returns an int. The decompiled assembly will actually be littered with these classes with mangled names, following a naming format of the target variable name (run) and an identifier (u004089). You can always decompile the computation expression to get a sense for how it’s been desugared.

Conclusion

I said in the beginning that monads are simple, but I’ll admit that I lied. Monads are tricky, there’s no denying that. Maybe that’s why there is no shortage of blog posts trying to explain the monad over and over again. But, in the end, once you wrap your head around it, I think computation expression syntax is a cool way of using the concept of a monad by decoupling what something is defined to do, vs how it’s actually executed.

I highly suggest running the examples and actually stepping through them bit by bit if you are having trouble following what is happening. Being able to see a desugared version of the code and using a debugger to inspect locals while stepping through examples makes it a lot clearer to see whats happening.

More reading

If you are curious here are some links to further reading explaining monads and computation expressions (such as the F# wikibook). Don Syme also has a few posts explaining things really well that are definitely worth checking out.