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:
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…
Leave a comment