<<

Gimme A Break (from types) We have more to do with type systems:  Parametric Polymorphism  CS152: Programming Languages Recursive Types  Type-And-Effect Systems

Lecture 14 — But sometimes it’s more fun to mix up the lecture schedule Evaluation Contexts First-Class This lecture: Related topics that work in typed or untyped settings: Lambda Interpreters  -Passing Style How operational can be defined more concisely  How lambda- (or PLs) can be enriched with powerful control operators  How lambda-calculus can be efficiently implemented Dan Grossman  Cool programming idioms related to these concepts Spring 2011 Dan Grossman CS152 Spring 2011, Lecture 14 2

Toward Evaluation Contexts Evaluation Contexts λ-calculus with extensions has many “boring inductive rules”: Define evaluation contexts, which are expressions with one hole     where “interesting work” is allowed to occur: e1 → e1 e2 → e2 e → e e → e     E ::= [·] | Ee| vE| (E, e) | (v, E) | E.1 | E.2 e1 e2 → e1 e2 ve2 → ve2 A(e) → A(e ) B(e) → B(e ) | A(E) | B(E) | (match E with Ax. e | By. e )     1 2 e1 → e1 e2 → e2 e → e e → e     Define “filling the hole” E[e] in the obvious way (stapling) (e1,e2) → (e1,e2) (v1,e2) → (v1,e2) e.1 → e .1 e.2 → e .2  A metafunction of type EvalContext→Exp→Exp e → e  Semantics: Use two judgments match e with Ax. e1 | By. e2 → match e with Ax. e1 | By. e2 p e → e  e → e with 1 rule: And some “interesting do-work rules”: E[e] → E[e] p  e → e (λx. e) v → e[v/x] (v1,v2).1 → v1 (v1,v2).2 → v2 with all the “interesting work”: p p p (λx. e) v → e[v/x] (v1,v2).1 → v1 (v1,v2).2 → v2 match A(v) with Ax. e | By. e → e [v/x] 1 2 1 p match A(v) with Ax. e1 | By. e2 → e1[v/x] match B(v) with Ay. e1 | Bx. e2 → e2[v/x] p match B(v) with Ay. e1 | Bx. e2 → e2[v/x] Dan Grossman CS152 Spring 2011, Lecture 14 3 Dan Grossman CS152 Spring 2011, Lecture 14 4

Decomposition Evaluation Contexts: So what? Evaluation relies on decomposition (unstapling the correct subtree) Small-step semantics (old) and evaluation- semantics (new) p  are very similar: Given e,findE, ea, ea such that e = E[ea] and ea → ea  Totally equivalent step sequence  (made both left-to-right call-by-value) (Unique Decomposition): There is at most one decomposition of e  Just rearranged things to be more concise: Each boring rule became a form of E  Hence evaluation is deterministic since at most one primitive  step can to any Both “work” the same way:  Find the next place in the program to take a “primitive step”  Take that step e Theorem (Progress, restated): If is well-typed, then there is a  Plug the result into the rest of the program decomposition or e is a value  Repeat (next “primitive step” could be somewhere else) until you can’t anymore (value or stuck) Evaluation contexts so far just cleanly separate the “find and plug” from the “take that step” by building an explicit E

Dan Grossman CS152 Spring 2011, Lecture 14 5 Dan Grossman CS152 Spring 2011, Lecture 14 6 Continuations Examples (exceptions-like) Now that we have defined E explicitly in our metalanguage,what if we also put it on our language 1+(letcc k. 2+3)→∗ 6  From metalanguage to language is called reification 1+(letcc k. 2+(throw k 3)) →∗ 4 First-class continuations in one slide: e ::= ...| letcc x. e | throw ee| cont E 1+(letcc k. (throw k (2 + 3))) →∗ 6 v ::= ...| cont E E ::= ...| throw Ee| throw vE 1+(letcc k. (throw k (throw k (throw k 2)))) →∗ 3

E[letcc x. e] → E[(λx. e)(cont E)] E[throw (cont E) v] → E[v]

p  New operational rules for → not → because “the E matters”  letcc x. e grabs the current evaluation context (“the stack”)  throw (cont E) v restores old context: “jump somewhere”  cont E not in source programs: “saved stack (value)”

Dan Grossman CS152 Spring 2011, Lecture 14 7 Dan Grossman CS152 Spring 2011, Lecture 14 8

Example (“time travel”) Is this useful? Caml doesn’t have first-class continuations, but if it did: First-class continuations are a single construct sufficient for:

let valOf x = match x with None-> failwith "" |Some x-> x  Exceptions let x = ref true (* avoids infinite loop ) let g = ref None  Cooperative threads (including ) lety=ref(1+2+(letcc k. (g := Some k); 3))  “yield” captures the continuation (the “how to resume me”) letz=if!x and gives it to the scheduler (implemented in the language), then (x := false; throw (valOf (!g)) 7) which then throws to another ’s “how to resume me” else !y  Other crazy things  SML/NJ does: This runs and binds 10 to z: Often called the “ of ” — incredibly powerful, but nonstandard uses are usually open SMLofNJ.Cont inscrutable val x = ref true (* avoids infinite loop *)  Key point is that we can “jump back in” unlike boring-old val g : int cont option ref = ref NONE exceptions valy=ref(1+2+(callcc (fn k => ((g := SOME k); 3)))) valz=if!xthen (x := false; throw (valOf (!g)) 7) else !y

Dan Grossman CS152 Spring 2011, Lecture 14 9 Dan Grossman CS152 Spring 2011, Lecture 14 10

Another view Where are we If you’re confused, think call stacks: Done:  Redefined our using evaluation contexts  What if your favorite language had operations for:  That made it easy to define first-class continuations  Store current stack in x   Replace current stack with stack in x Example uses of continuations Now:  “Resume the stack’s hole” with something different or when  Implement an efficent lambda-calculus interpreter using little mutable state is different more than malloc and a single while-loop  Else you are sure to have an infinite loop since you will later  Explicit evaluation contexts (i.e., continuations) is essential resume the stack again  Key novelty is maintaining the current context incrementally  letcc and throw can be O(1) operations (homework 4)  Can achieve the same effect as a source-to-source transformation  The continuation-passing-style (CPS) transformation  Programming in explicit CPS-style is also a powerful idiom

Dan Grossman CS152 Spring 2011, Lecture 14 11 Dan Grossman CS152 Spring 2011, Lecture 14 12 See the code Example See lec14_interp.ml for four interpreters where each is: Small-step (first interpreter):  More efficient than the previous one and relies on less from the meta-language  Close enough to the previous one that equivalence among them is tractable to prove

The interpreters: 1. Plain-old small-step with Decomposition (second interpreter): 2. Evaluation contexts, re-decomposing at each step 3. Incremental decomposition, made efficient by representing evaluation contexts (i.e., continuations) as a linked list with “shallow end” of the stack at the beginning of the list 4. Replacing substitution with environments

The last interpreter is trivial to port to assembly or

Dan Grossman CS152 Spring 2011, Lecture 14 13 Dan Grossman CS152 Spring 2011, Lecture 14 14

Example Example Decomposition (second interpreter): Decomposition rewritten with linked list (hole implicit at front):

Some loop iterations of third interpreter:

Decomposition rewritten with linked list (hole implicit at front):

Fourth interpreter: replace substitution with environment/closures Dan Grossman CS152 Spring 2011, Lecture 14 15 Dan Grossman CS152 Spring 2011, Lecture 14 16

The end result Toward the CPS transform The last interpreter needs just: Rather than a fancy abstract machine where...  Aloop  The metalanguage doesn’t need (a call-stack)  Lists for contexts and environments  letcc/throw are easy-to-encode O(1) operations  Tag tests ... can achieve the same effect via a whole-program translation Moreover: into a sublanguage (source-to-source transformation)  calls execute in O(1) time  No expressions with nontrivial evaluation contexts  look-ups don’t, but that’s fixable  Every expression becomes a continuation-accepting function  (e.g., de Bruijn indices and arrays for environments)  Never “return” — instead call the current continuation  Other operations, including pairs, conditionals, letcc, and throw also all work in O(1) time  Need new kinds of contexts and values  Last problem of homework 4

Making evaluation contexts explicit data structures was key

Dan Grossman CS152 Spring 2011, Lecture 14 17 Dan Grossman CS152 Spring 2011, Lecture 14 18 The CPS transformation (one way to do it) Result of the CPS transformation

A metafunction from expressions to expressions  Correctness: e is equivalent to CPSE(e) λx. x  If whole program has type τP and e has type τ ,then Example source language (other features similar): CPSE(e) has type (τ → τP ) → τP  CPS (e) e e ::= x | λx. e | ee| c | e + e Fixes evaluation order: E will evaluate in left-to-right call-by-value v ::= x | λx. e | c  Other similar transformations encode other evaluation orders  Every intermediate is bound to a variable (helpful CPSE(v)=λk. k CPSV(v) for compiler writers) CPSE(e1 + e2)=λk. CPSE(e1) λx1. CPSE(e2) λx2.k(x1+x2)  For all e, evaluation of CPSE(e) stays in this sublanguage: CPSE(e1 e2)=λk. CPSE(e1) λf. CPSE(e2) λx. f x k e ::= v | vv| vvv| v (v + v)

CPSV(c)=c v ::= x | λx. e | c CPSV(x)=x  Hence no need for a call-stack: every call is a tail-call CPSV(λx. e)=λx. λk. CPSE(e) k  Now the program is maintaining the evaluation context via a closure that has the next “link” in its environment that has the To run the whole program e,doCPSE(e) λx. x next “link” in its environment, etc.

Dan Grossman CS152 Spring 2011, Lecture 14 19 Dan Grossman CS152 Spring 2011, Lecture 14 20

Encoding first-class continuations A useful advanced programming idiom If you apply the CPS transform, then letcc and throw can become  O(1) operations encodable in the source language A first-class continuation can “reify session state” in a client-server interaction  If the continuation is passed to the client, which returns it CPSE(letcc k. e)=λk. CPSE(e) k later, then the server can be stateless  CPSE(throw e1 e2)=λk. CPSE(e1) λx1. CPSE(e2) λx2.x1 x2 Suggests CPS for web programming     Better: tools that do the CPS transformation for you or just x1  Gives you a “prompt-client” primitive without server-side state

  letcc gets passed the current continuation just as it needs Because CPS uses only tail calls, it avoids deep call stacks when traversing recursive data structures  throw ignores the current continuation just as it should  See lec14_tailcalls.ml for this and related idioms

You can also manually program in this style (fully or partially) In short, “thinking in terms of CPS” is a powerful technique few  Has other uses as a programming idiom too... programmers have

Dan Grossman CS152 Spring 2011, Lecture 14 21 Dan Grossman CS152 Spring 2011, Lecture 14 22