Continuations in TypeScript

Abstracting common patterns in TypeScript

Wim Jongeneel
ITNEXT

--

A major challenge while developing an user interface is dealing with the events that are triggered by user interaction. Not only are there often many possible events that can occur at any given time, the order in which events get triggered is also variable (or they can happen simultaneously). Outside of those events JavaScript applications has to deal with other asynchronous code like handling API calls.

During the decades in which JavaScript evolved to where we are today there have been a multitude of ways to write async code. From the event listeners in html, the callback-hell of JQuery and the onChange property of your React components, to just name a few most people will recognize. And in this wild journey there have been some questionable patterns and ad-hoc solutions that turned out to be not as ideal as hoped for.

In this article I want to look a way of dealing with this idea of ‘code that happens when something else has happened’ that has a more formal and mathematical background to hopefully help you get an intuition for the common foundations of all those patterns. This is in no means a replacement for them, but an exposition of a way to logically reason about all of this.

What is a continuation?

All ways of dealing with events have some conceptual similarities: we have a bit of code that runs after something else happened. In all the examples from the introduction you can see that no-matter which way of dealing with events we use we have two things: something we wait for to happen and something we do after. In terms of imperative code we could say we wait until we continue execution of our code. This is where the continuation get its name from: we continue after something has happened. So what does a continuation look like? Well actually pretty simple. They are nothing more than a function that gets a callback as argument:

Note that the long name ‘continuation’ commonly gets shorted to ‘cont’

While this is the correct definition of a continuation I want to use an slightly different one that wraps the type above in an interface. This has the benefit of use being able to use a fluent api for the operations we will define for continuations:

As you can see, the definition of the run property is exactly the same as the definition before. Within this type we can add more methods to the Cont type to define all our operations. But what exactly is a continuation?

With old-skool JavaScript callbacks we are really focussing on the callback: the entire pattern is centered around providing callbacks to async functions that get invoked when those functions are done. With continuations we like to reason the other way around: we have things that will output values at a certain time. The focus here is the thing-that-outputs instead of the logic that happens after. With continuations we will reason and program around this object that has not yet a value, but will eventually have one. For JavaScript this has the additional benefit of not really having to care on how we register the event listeners (something that changes all the time) but instead hide this behind a library of those objects that will eventually contain values. The callback we will provide is now nothing more than a connector between the world of continuations and the rest of our code.

Defining the continuation

Readers who are familiar with functional programming will have noticed a common pattern in the previous paragraph. Within functional programming a lot of the exciting stuff happens around abstractions that work like a magic box with a value inside. And exactly, it turns out that our pattern of a callback is indeed a monad. Don’t worry if you don’t get monads (yet), I will attempt to keep everything practical and illustrated with examples.

The first property of a continuation is that we can transform its output from one type to another type. Image you have a keyboard continuation that outputs a JavaScript Event when a button gets pressed and you a function displayKeycode that prints a string. With the map function you can transform the keyboard continuation to a continuation of a string and provide your callback to the result. An example of how this could look like is shown below:

map is a method that takes a function from the type your continuation is (a) to the type you want your continuation to be (commonly denoted as b). With map we can combine continuations and callbacks of incompatible types together. The new definition of our Cont type looks like this:

The other method that turns a continuation into a monad is the bind method. However, in TypeScript the name bind is already taken for binding the scope of functions, so I will use then instead. then is a methods that creates a new continuation based on the value of the continuation we already have. With this we can chain multiple continuations together into a bigger asynchronous processes. Lets go back to our previous example with the keyboard continuation and image that we want to add some delay between the button being pressed and the code being shown. For this we could use a continuation wait that outputs the value that you gave it after a certain amount of time (actually setTimeout as a continuation). The code we would write with this would look like the following:

If we add then to our definition of Cont then we end up with its full definition as shown below. For those who are interested, the code to construct a Cont from a function that calls a callback can be found in this snippet.

Continuations and promises

Promises have been a very welcome addition to JavaScript to end the callback-hell most people who have ever worked with JQuery will remember. And it happens to be that promises and continuations are based on very similar ideas. There are however some major differences. One of them is that promises have an auto-join mechanisme that turns every promise of a promise into directly into a promise. This has as a result that map and then on a promise are the same (hence the absence of a map method on promises). Another major difference is that promises only output once instead of potentially many times. Finally a promise has both a then and a catch handler for error handling instead of just a single handler (see my article on railroad programming for a functional approach to error handling that works great with continuations).

But at the end of the day both promises and continuations deal with async code so it is possible to convert a promise into a continuation (note that this will always output once):

An important property of promises is that they are eager, meaning that when you have a promise of a certain process that process is always running. This can be a bit strange with our library where a continuation only starts outputting things when we called run. With the promise adapter shown above it is possible we will ‘miss’ the value when the promise settels before we added a callback to it. For this it is more practical to use a lazy promise. A lazy promise is a function that returns a promise from zero arguments. Because the promise only constructed after we called this function, we can control when the async process starts running and integrate it nicely with our continuations.

Continuation combinators

Now we have defined our abstraction and seen some examples it is time to look to some more advanced concepts. One of the very powerful properties of continuations is that they support a large amount of combinators. A combinator is a function that takes multiple continuations and turns them into one single continuation.

There are various common combinators that are very generic and reusable, but you can also create very specific combinators to serve you own needs. The most common generic combinator I want to look at first is any. any takes an array of continuations and returns a continuation that outputs a value whenever one of the continuations outputs a value.

This is the most used combinator as it is the most natural way of combining the things-that-output into a single thing that outputs. Note that this combinator is impossible to define for promises as the promise you would return can only outputs once. Another common combinator is race. race is also defined for promises and does the same thing for continations:

An interesting fact to note about the implementation of race is that we reuse any, but limit it to output only once. How once is implemented will be shown in the next chapter. (actually an attempt to implement any for promises will likely result in race) Outside of those two we could also make an all that waits for all continuations to output, a zip that gives you pairs of results if two continuation outputted, etc.

Continuations decorators

A higher-order-continuation is a continuation that gets constructed from another continuation. This pattern is also known as the decorator pattern in OOP. once is such a decorator that limits a continuation to only one output, essentially turning it into a promise.

Another very useful decorator is filter. filter filters the outputs of a continuation with a predicate. This function could also be added to the fluent api we created for our continuation to be able to call filter in a similar way we use filter on an array.

An interesting observation to make here is that a continuation works like a collection of values. When we converted promises to continuations we already noted that continuations can output multiple values, but now we see that they can be treated as real collections. They only output their values over time instead of being able to output all of them instantly (some may also know this as an asynchronous stream). This means that all the methods that exist for collections can also be implemented for continuations. For example, skip and take can be implemented for continuations:

Conclusion

Some of you might have recognized a lot of the concept that I described from RxJS. And indeed RxJs is a library that is based around the same concepts, but has been written with more attention to real-life problem solving than to mathematical perfection (which is totally the right thing to do for a library you intent to actually use).

If you really liked reading about those concepts and are wondering how they can be applied into a broader concept you can read the article linked below on monadic react. This is a library that uses a renderable variant on the continuation to wrap react components. By doing this it is possible to map, bind, combine and decorate react components in the exact same way we did with our continuations.

--

--

Writer for

Software Engineer at Mendix (Rotterdam, The Netherlands) • Student MSc Software Engineering • Functional programming enthusiast