Lifting Primitive Operations to Computations

I’ll demonstrate how and why Atum lifts primitive operations to computations in the delimited continuation monad. The result is the first version of an ECMAScript interpreter: a very simple calculator.

Lifting and Composition

I previously defined a small library of primitive hosted value types and operations. But those function have no concept of delimited control and cannot be used directly with the delimited continuation monad. A few higher-order functions handle this problem.

The converted forms can now be used directly in delimited continuation monad computations.

run(bind(boolean(true),toString),console.log);// prints string "true"

Lift

lift takes a unary function f and returns a new function that performs f in the monadic context. The result of lift is a unary function takes a monadic value m as its argument, and returns a monadic result of f applied to the result of the input computation.

Lifting eliminates a lot of boilerplate wrapping and unwrapping of values.

varaddM=lift2(number_value.add);run(addM(number(3),addM(number(5),number(1))),console.log);// number 9

Composition

One other useful operation is to compose two monadic functions. Kleisli composition takes two unary functions f and g that map values to computations, and returns a new function that maps to the result of f and g sequenced with bind.

varcompose=function(f,g){returnfunction(x){returnbind(f(x),g);};};

compose composes f and g left-to-right. The isTrue computation for example checks if a hosted value is truthy:

varisTrue=compose(toBoolean,from(boolean_value.isTrue));

Number Computations

We can now start building up a library of ECMAScript computations by applying lift and from to the primitive value operations.

Number Operations

All of the binary number value operations convert both of their arguments to numbers.

_binaryOperation lifts a primitive number operations and convert its arguments using two type conversion computations.

Computations are evaluated by evaluate with a outermost continuation k.

varevaluate=function(c,k){returnc(cons(k,NIL));};

This primitive interpreter only understands binary literal expressions. evaluateText uses Parse-ECMA to parse some ECMAScript source text, and evaluate the first binary expression from the program body.

Why

So what does all this work get us? It is certainly possible to implement a calculator, or even an entire ECMAScript interpreter, using standard CPS programming. But I find there are two primary practical benefits to building an interpreter using monads in an untyped language: it becomes easier to add new core functionality to the interpreter, and monads are one good way to express and compose computations at a higher level.

Even though none of the number computations definitions above ever mention continuations, this computation just works.

Behind the interface of just and bind, the delimited continuation monad implements delimited control to apply and sequence continuations. But computations and higher-order function only need to know about just and bind and not about delimited control.

The other benefit is that it becomes easy to add functionality to the interpreter by changing the underling monad. In fact, I’ll demonstrate this in the next post by adding state to the interpreter, and all the computations defined here will continue to work fine.