Understanding continuations

How 'let' works behind the scenes

In the previous post we saw how some complex code could be condensed using computation expressions.

Here's the code before using a computation expression:

let log p = printfn "expression is %A" p

let loggedWorkflow = 
    let x = 42
    log x
    let y = 43
    log y
    let z = x + y
    log z
    //return
    z

And here's the same code after using a computation expression:

let loggedWorkflow = 
    logger
        {
        let! x = 42
        let! y = 43
        let! z = x + y
        return z
        }

The use of let! rather than a normal let is important. Can we emulate this ourselves so we can understand what is going on? Yes, but we need to understand continuations first.

Continuations

In imperative programming, we have the concept of "returning" from a function. When you call a function, you "go in", and then you "come out", just like pushing and popping a stack.

Here is some typical C# code which works like this. Notice the use of the return keyword.

You've seen this a million times, but there is a subtle point about this approach that you might not have considered: the called function always decides what to do.

For example, the implemention of Divide has decided that it is going to throw an exception. But what if I don't want an exception? Maybe I want a nullable<int>, or maybe I am going to display it on a screen as "#DIV/0". Why throw an exception that I am immediately going to have to catch? In other words, why not let the caller decide what should happen, rather the callee.

Similarly in the IsEven example, what am I going to do with the boolean return value? Branch on it? Or maybe print it in a report? I don't know, but again, rather than returning a boolean that the caller has to deal with, why not let the caller tell the callee what to do next?

So this is what continuations are. A continuation is simply a function that you pass into another function to tell it what to do next.

Here's the same C# code rewritten to allow the caller to pass in functions which the callee uses to handle each case. If it helps, you can think of this as somewhat analogous to a visitor pattern. Or maybe not.

Note that the C# functions have been changed to return a generic T now, and both continuations are a Func that returns a T.

Well, passing in lots of Func parameters always looks pretty ugly in C#, so it is not done very often. But passing functions is easy in F#, so let's see how this code ports over.

Here's the "before" code:

and here's the "after" code:

A few things to note. First, you can see that I have put the extra functions (ifZero, etc) first in the parameter list, rather than last, as in the C# example. Why? Because I am probably going to want to use partial application.

And also, in the isEven example, I wrote aNumber |> ifEven and aNumber |> ifOdd. This makes it clear that we are piping the current value into the continuation and the continuation is always the very last step to be evaluated. We will be using this exact same pattern later in this post, so make sure you understand what is going on here.

Continuation examples

With the power of continuations at our disposal, we can use the same divide function in three completely different ways, depending on what the caller wants.

Here are three scenarios we can create quickly:

  • pipe the result into a message and print it,

  • convert the result to an option using None for the bad case and Some for the good case,

  • or throw an exception in the bad case and just return the result in the good case.

Notice that with this approach, the caller never has to catch an exception from divide anywhere. The caller decides whether an exception will be thrown, not the callee. So not only has the divide function become much more reusable in different contexts, but the cyclomatic complexity has just dropped a level as well.

The same three scenarios can be applied to the isEven implementation:

In this case, the benefits are subtler, but the same: the caller never had to handle booleans with an if/then/else anywhere. There is less complexity and less chance of error.

It might seem like a trivial difference, but by passing functions around like this, we can use all our favorite functional techniques such as composition, partial application, and so on.

We have also met continuations before, in the series on designing with types. We saw that their use enabled the caller to decide what would happen in case of possible validation errors in a constructor, rather than just throwing an exception.

The success function takes the email as a parameter and the error function takes a string. Both functions must return the same type, but the type is up to you.

And here is a simple example of the continuations in use. Both functions do a printf, and return nothing (i.e. unit).

Continuation passing style

Using continuations like this leads to a style of programming called "continuation passing style" (or CPS), whereby every function is called with an extra "what to do next" function parameter.

To see the difference, let's look at the standard, direct style of programming.

When you use the direct style, you go "in" and "out" of functions, like this

In continuation passing style, on the other hand, you end up with a chain of functions, like this:

There is obviously a big difference between the two styles.

In the direct style, there is a hierarchy of functions. The top level function is a sort of "master controller" who calls one subroutine, and then another, deciding when to branch, when to loop, and generally coordinating the control flow explicitly.

In the contination passing style, though, there is no "master controller". Instead there is a sort of "pipeline", not of data but of control flow, where the "function in charge" changes as the execution logic flows through the pipe.

If you have ever attached a event handler to a button click in a GUI, or used a callback with BeginInvoke, then you have used this style without being aware of it. And in fact, this style will be key to understanding the async workflow, which I'll discuss later in this series.

Continuations and 'let' ##

So how does all this fit in with let?

Let's go back and revisit what 'let` actually does.

Remember that a (non-top-level) "let" can never be used in isolation -- it must always be part of a larger code block.

That is:

really means:

And then every time you see the x in the second expression (the body expression), substitute it with the first expression (someExpression).

So for example, the expression:

really means (using the verbose in keyword):

Now funnily enough, a lambda looks very similar to a let:

and if we pipe in the value of x as well, we get the following:

Doesn't this look awfully like a let to you? Here is a let and a lambda side by side:

They both have an x, and a someExpression, and everywhere you see x in the body of the lambda you replace it with someExpression. Yes, the x and the someExpression are reversed in the lambda case, but otherwise it is basically the same thing as a let.

So, using this technique, we can rewrite the original example in this style:

When it is written this way, you can see that we have transformed the let style into a continuation passing style!

  • In the first line we have a value 42 -- what do we want to do with it? Let's pass it into a continuation, just as we did with the isEven function earlier. And in the context of the continuation, we will relabel 42 as x.

  • In the second line we have a value 43 -- what do we want to do with it? Let's pass it too into a continuation, calling it y in that context.

  • In the third line we add the x and y together to create a new value. And what do we want to do with it? Another continuation, another label (z).

  • Finally in the last line we are done and the whole expression evaluates to z.

Wrapping the continuation in a function

Let's get rid of the explicit pipe and write a little function to wrap this logic. We can't call it "let" because that is a reserved word, and more importantly, the parameters are backwards from 'let'. The "x" is on the right hand side, and the "someExpression" is on the left hand side. So we'll call it pipeInto for now.

The definition of pipeInto is really obvious:

Note that we are passing both parameters in at once using a tuple rather than as two distinct parameters separated by whitespace. They will always come as a pair.

So, with this pipeInto function we can then rewrite the example once more as:

or we can eliminate the indents and write it like this:

You might be thinking: so what? Why bother to wrap the pipe into a function?

The answer is that we can add extra code in the pipeInto function to do stuff "behine the scenes", just as in a computation expression.

The "logging" example revisited ###

Let's redefine pipeInto to add a little bit of logging, like this:

Now... run that code again.

What is the output?

This is exactly the same output as we had in the earlier implementations. We have created our own little computation expression workflow!

If we compare this side by side with the computation expression version, we can see that our homebrew version is very similar to the let!, except that we have the parameters reversed, and we have the explicit arrow for the continuation.

computation expression: logging

The "safe divide" example revisited ###

Let's do the same thing with the "safe divide" example. Here was the original code:

You should see now that this "stepped" style is an obvious clue that we really should be using continuations.

Let's see if we can add extra code to pipeInto to do the matching for us. The logic we want is:

  • If the someExpression parameter is None, then don't call the continuation lambda.

  • If the someExpression parameter is Some, then do call the continuation lambda, passing in the contents of the Some.

Here it is:

With this new version of pipeInto we can rewrite the original code like this:

We can clean this up quite a bit.

First we can eliminate the a, b and c, and replace them with the divideBy expression directly. So that this:

becomes just this:

Now we can relabel a' as just a, and so on, and we can also remove the stepped indentation, so that we get this:

Finally, we'll create a little helper function called return' to wrap the result in an option. Putting it all together, the code looks like this:

Again, if we compare this side by side with the computation expression version, we can see that our homebrew version is identical in meaning. Only the syntax is different.

computation expression: logging

Summary

In this post, we talked about continuations and continuation passing style, and how we can think of let as a nice syntax for doing continuations behind scenes.

So now we have everything we need to start creating our own version of let. In the next post, we'll put this knowledge into practice.

Last updated

Was this helpful?