Chapter 19. Continuation-Passing Style

One can approach the notion of continuation from many different angles. I would like to start with asynchronous function calls in Javascript (JS), which are often employed for the purpose of eliminating potential blocking due to inputs or resources needed for execution being yet to be made available.

Let us use do_async1 and do_async2 to refer to two functions such that calls made to them are supposed to be executed asynchronously (so as to avoid potential blocking of entire program execution):

// fun do_async1(): void fun do_async2(): void //

In the body of the following function do_async12, a call to do_async1 is followed by another call to do_async2:

// fun do_async12(): void = { val () = do_async1(); val () = do_async2() } (* end of [do_async12] *) //

Calling do_async1 returns immediately after the actual work to be done is scheduled. And the same can be said about calling do_async2. There is in general no guarantee that the work scheduled for calling do_async1 is done ahead of the work scheduled for calling do_async2 or vice versa. What needs to be done if we want to enforce such an order? One solution is to provide a variant of do_async1 of the following interface:

// fun do_async1_cont(k: cfun(void)): void //

The argument passed to do_async1_cont is a thunk (that is, a nullary closure-function), which is supposed to be called immediately after the work scheduled for do_async1 is done. And this thunk is often given the name continuation, referring to the work that needs to be continued. In the following presentatin, cont0() may also be used as a shorthand for cfun(void). With do_async1_cont, the function do_async1() can be given the following implementation:

// fun do_async1 ( // argless ) : void = do_async1_cont(lam() => ())

which simply means that no more work is needed after the work scheduled for calling do_async1 is done. As for the function do_async12(), the following implementation can be given:

// fun do_async12(): void = { val () = do_async1_cont(lam() => do_async2()) } (* end of [do_async12] *) //

where a call to do_async2 is to be made only after the work scheduled for do_async1 is finished. In fact, we can require that each function take a continuation argument if it ever schedules work to be done asynchronously. For instance, we can have the following variant of do_async12:

// fun do_async12_cont (k: cont0()): void = { val () = do_async1_cont(lam() => do_async2_cont(k)) } (* end of [do_async12] *) //

where the interface for do_async2_cont is just like that for do_async1_cont. And this style of coding is often referred to as continuation-passing style (CPS).

In the following presentation, I would like to present a few more examples that contrast the direct style with the CPS-style. Let cont1 be declared as follows:

typedef cont1(res:t@ype) = (res) -<cloref1> void

The functions fact and k_fact given below are implemented in the direct style and the CPS-style, respectively:

// fun fact ( n : int ) : int = if n > 0 then n * fact(n-1) else 1 // (* ****** ****** *) // fun k_fact ( n : int , k : cont1(int) ) : void = ( // if (n > 0) then k_fact(n-1, lam(res) => k(n*res)) else k(1) // end of [if] ) (* end of [k_fact] *) //

Given an integer n and a continuation k, k_fact(n, k) basically means to pass fact(n) to the continuation k. Please note that the continuation argument passed to the recursive call in the body of k_fact is a newly built closure-function that multiplies by n whatever integer passed to it and then passes the product to the original continuation.

The next example is slightly more involved. The functions fibo and k_fibo given below are implemented in the direct style and the CPS-style, respectively:

// fun fibo ( n : int ) : int = if n >= 2 then fibo(n-1)+fibo(n-2) else n // (* ****** ****** *) // fun k_fibo ( n : int , k : cont1(int) ) : void = ( // if (n >= 2) then k_fibo ( n-1 , lam(r1) => k_fibo(n-2, lam(r2) => k(r1+r2)) ) (* end of [then] *) else k(n) // end of [else] // end of [if] ) (* end of [k_fibo] *) //

Given an integer n and a continuation k, k_fibo(n, k) basically means to pass fibo(n) to the continuation k. There are two recursive calls in the body of fibo; they are translated into two recurisve calls in the body of k_fibo, where the second one appears in the continuation passed to the first one.

Going from fact to k_fact and fibo to k_fibo is often referred to as CPS-translation. Note that the recursive calls in the bodies of k_fact and k_fibo are all tail-calls. In general, each recursive call in an implementation written in the direct style should be translated into a tail-recursive one in the corresponding implementation written in the CPS-style. If a recursive call in the direct style happens to be tail-recursive, then the corresponding tail-recursive call in the CPS-style is passed the same continuation as the one passed to its caller. For instance, the following example shows a tail-recursive function (fact2) in the direct style being translated into one (k_fact2) in the CPS-style:

// fun fact2 (n : int, res: int): int = if n > 0 then fact2(n-1, n*res) else res // (* ****** ****** *) // fun k_fact2 (n: int, res: int, k: cont1(int)): void = if n > 0 then k_fact2(n-1, n*res, k) else k(res) //

As yet another example, let us see list0_map of the direct style contrasted with list0_kmap of the CPS-style:

// extern fun {a:t@ype} {b:t@ype} list0_map (xs: list0(INV(a)), f0: cfun(a, b)): list0(b) extern fun {a:t@ype} {b:t@ype} list0_kmap ( xs: list0(INV(a)) , f0: cfun(a, cont1(b), void), k0: cont1(list0(b))): void // (* ****** ****** *) // implement {a}{b} list0_map(xs, f0) = ( case+ xs of | list0_nil() => list0_nil() | list0_cons(x, xs) => list0_cons(f0(x), list0_map<a><b>(xs, f0)) ) // implement {a}{b} list0_kmap(xs, f0, k0) = ( case+ xs of | list0_nil() => k0(list0_nil()) | list0_cons(x, xs) => f0(x, lam(y) => list0_kmap<a><b>(xs, f0, lam(ys) => k0(list0_cons(y, ys)))) ) //

Note that the argument f0 (for list0_kmap) takes a continuation as its second argument, which makes f0 a second-order function. Therefore, list0_kmap is itself a third-order function.

The last example in this chapter is the following stream-processing function written in CPS-style:

// extern fun {a:t@ype} stream_kforeach ( xs: stream(INV(a)) , f0: cfun(a, cont1(bool), void), k0: cont0()): void implement {a}(*tmp*) stream_kforeach(xs, f0, k0) = ( case+ !xs of | stream_nil() => k0() | stream_cons(x, xs) => f0(x, lam(y) => if y then stream_kforeach<a>(xs, f0, k0) else k0()) ) //

Given a stream and a function, stream_kforeach processes an element in the given stream by applying the given function to it; if the function returns true, then stream_kforeach repeats to process the next element (if it exists); if the function returns false, then stream_kforeach stops processing the rest of the elements and terminates. Of course, what is described here is actually done in CPS-style. Please click here to see a demo of stream_kforeach being called to display the (infinite) stream of prime numbers: 2, 3, 5, 7, 11, etc. For the source code of demo, please see Sieve.dats.

Please find on-line the entirety of the code used in this chapter. The mentioned URL link(s) can be found as follows: