Lecture 4: Continuation Passing Style

Teacher: François Pottier

CPS transform

Continuation Passing Style VS Direct-style

  • CPS transform any recursive program into an iterative one
  • CBN/CBV: don’t matter much anymore after the transformation
  • In CPS: function calls only (no return)
  • No control stack

Direct style interpreter

Environment-based CBV interpreter:

    let rec eval (e : cenv) (t : term) : cvalue =
    match t with
    | Var x ->
        lookup e x
    | Lam t ->
        Clo (t, e)
    | App (t1, t2) ->
        let cv1 = eval e t1 in let cv2 = eval e t2 in let Clo (u1, e) = cv1 in eval (cv2 :: e) u1

CPS:

let rec eval (e : cenv) (t : term) : cvalue =

is turned into

let rec evalk (e : cenv) (t : term) (k : cvalue -> a) : a =

CPS interpreter:

let rec evalk (e : cenv) (t : term) (k : cvalue -> a) : a = match t with
    | Var x ->
        k (lookup e x)
    | Lam t ->
        k (Clo (t, e))
    | App (t1, t2) ->
        evalk e t1 (fun cv1 ->
            evalk e t2 (fun cv2 ->
                let Clo (u1, e) = cv1 in
                evalk (cv2 :: e) u1 k))

NB: every argument is a value now ($λ$-abstraction or actual value), so no difference between CBN and CBV anymore.

Run it with the identity continuation:

let eval (e : cenv) (t : term) : cvalue =
    evalk e t (fun cv -> cv)

Tail calls: evalk is the last call done in each call ⟶ tail calls. Tail calls have a special status in most functional languages: they’re treated in a special way by the compiler (with a GOTO): no need to store anything on the stack.

Example tt= term in tail position / nt = not in tail position:

λx. \underbrace{\overbrace{f}^{\texttt{nt}} \, (\overbrace{g}^{\texttt{nt}} \, \overbrace{x}^{\texttt{nt}})}_{\texttt{tt}}

NB: tail calls are not necessarily recursive:

let f x = g (x+1)

In OCamL, you can add annotations so that the compiler checks that there is a tail call (and fails otherwise):

(k[@tailcall]) (lookup e x)

This CPS interpreter is iterative: what was stored on the stack is now in the heap (useful to capture/store the current continuation ⟶ simple way to implement control operators).

CPS through Curry-Howard: CPS transformed proofs: $T$ is turned into $(T ⟶ α) ⟶ α$. With $α = 0$: we have $¬¬T$ ⟶ $¬¬$-translation

 type kont =
    | AppL of { e: cenv; t2: term; k: kont }
    | AppR of { cv1: cvalue; k: kont }
    | Init
 let rec evalkd (e : cenv) (t : term) (k : kont) : cvalue =
    match t with
    | Var x ->
        apply k (lookup e x)
    | Lam t ->
        apply k (Clo (t, e))
    | App (t1, t2) ->
        evalkd e t1 (AppL { e; t2; k })

and apply (k : kont) (cv : cvalue) : cvalue =
    match k with
    | AppL { e; t2; k } ->
        let cv1 = cv in
        evalkd e t2 (AppR { cv1; k })
    | AppR { cv1; k } ->
        let cv2 = cv in
        let Clo (u1, e) = cv1 in
        evalkd (cv2 :: e) u1 k
| Init -> cv

and then

let eval e t = evalkd e t Init

CBV CPS transformation

⟦x⟧ = λk. k \, x\\ ⟦λx.t⟧ = λk. k(λx. ⟦t⟧)\\ ⟦t_1 \, t_2⟧ = λk. ⟦t_1⟧ (λx_1. ⟦t_2⟧ (λx_2. x_1 \, x_2 \, k))\\ ⟦ \texttt{let } x=t_1 \texttt{ in } t_2⟧ = λk. ⟦t_1⟧ \, (λx. ⟦t_2⟧ k)

NB: $fv(⟦t⟧) = fv(t)$

In first week: we asked “is it possible to encode CBN into CBV”? (yes, with thunks) Other direction (CBV into CBN): with CPS, as the resulting code is indifferent (so it can be run by a CBN interpreter)

Every call is a tail call.

About types:

⟦α⟧ = α\\ ⟦T_1 → T_2⟧ = ⟦T_1⟧ → ((⟦T_2⟧ → A) → A)

If $A$ is taken to be the empty type: we go from $T_2$ to $¬¬T_2$ ⟶ double negation translation

With polymorphism and linear types:

∀A. (⟦T⟧ → A) ⊸ A

But not with control effects. Control operators: enable you to capture the current continuation: call/cc (in Scheme), shift/reset, Landin’s J operator, effect handlers (in Scala, Koka, Links, Eff, an experimental version of OCamL), etc…

\begin{xy} \xymatrix{ t \ar[r]^{ cbv } \ar@{.>}[d] & t' \ar@{.>}[d] \\ ⟦t⟧\, k \ar[r]_{ cbn \text{ or } cbv }^\ast & ⟦t'⟧\, k } \end{xy}

Leave a comment