520 likes | 609 Views
The Marriage of Dependent Types and Effects. Greg Morrisett Joint with A.Nanevski (MSR), L.Birkedal (ITU), R.Peterson (ITU), P.Govereau, R.Wisnesky. The Marriage of Dependent Types and Effects. Greg Morris s ett e
E N D
The Marriage of Dependent Types and Effects Greg Morrisett Joint with A.Nanevski (MSR), L.Birkedal (ITU), R.Peterson (ITU), P.Govereau, R.Wisnesky
The Marriage of Dependent Types and Effects Greg Morrissette Joint with A.Nanevski (MSR), L.Birkedal (ITU), R.Peterson (ITU), P.Govereau, R.Wisnesky
From SML to ZML? A discussion at the last ML workshop: Things people generally agreed upon: • better support for refinement (and thus dependent) types • better treatment of effects • better support for concurrency Code name: YML This talk : Y0 (a.k.a. Y-naught, Y-not, Y-knot, …) Next year : Y-succ
Arrays/Vectors An embarrassment to ML: sub : array int So a buffer can cause your server to crash with an uncaught exception. • either refine domain of sub, • or at least reflect when it throws
What we desire: sub : x:array , {i:int | 0 <= i < size x} csub : x:array ,i:int, • throws IndexOutOfBounds when i < 0 or i > size x But this raises a lot of subtle questions: • how to introduce and eliminate refinements? • what is the language of refinements? • how to rectify effects with dependency? • …
What does this mean? sub : x:array , {i:int | 0 <= i < size x} -> let x : array int = #[0,1,2,3 ] i : ref int = ref 3 in sub x (i := rand() mod 3; fork g; !i) end
DML [Xi & Pfenning] • Add modeling types at the kind level: • ::= * | Nat | Bool • ::= 0 | Succ | NatRec | … • Connect type and term levels with singletons • 0 : S(0), succ : n:Nat, S(n) S(Succn) • Limited predicates on modeling types: • sub : n,m:Nat [nm]array nS(m) • Many follow-ons • ATS, Omega, Concoqtion, …
DML-style dependency Pros: • conservative over ML • avoids issues with effects • strong phase separation Cons: • in practice, lots of duplication • not just in meta-theory, but in the code. • can’t say anything about computations • can’t say much about state, IO, exns, …
An Alternative: Y0 Started with type theory. • we’re doing a shallow embedding in Coq. • (but other environments might do as well) • keys: support for dependency, refinement, etc. • and extraction to ML or Haskell Add a distinct type of effectful computations. • monadic types as in Haskell: ◊A • but indexed with predicates: ◊{P}A{Q}. • includes recursion, (h.o.) state, exns, … • lots of attention to frame, modularity conditions. More or less “pay-as-you-go.”
A Very Simple Example: Definition postinc x := xv <-- !!x ; x ::= (xv + 1) ;; ret xv. You can extract the corresponding Haskell: postinc x = do {xv <- read x ; write x (xv + 1) ; return xv }
Expanding the Definition Definition postinc x := xv <-- !!x ; x ::= (xv + 1) ;; ret xv. Expands into: Definition postinc x := bind (read x) ( xv => bind (write x (xv + 1)) ( _ => ret xv)).
The ST Monad in Coq ST(P:pre)(A:Set)(Q:post A) : Set Read as Hoare-partial correctness: • a computation which • when run in a heap satisfying P, • diverges or else produces a value of type A and a heap satisfying Q.
Roadmap ST(P:pre)(A:Set)(Q:post A) : Set • Heaps, pre- & post-conditions • Basic monadic & state constructs • Exceptions, conditionals, arrays • Example: hashtable • Recursion and meta-theoretic issues • Modularity & separation • Current & future work
Heaps, Pre & Post-Conditions ST(P:pre)(A:Set)(Q:post A) : Set. pre := heap->Prop post(A:Set) := A->heap->heap->Prop. loc : Set. heap := loc->option Dynamic. Dynamic := {A:Set ; v:A }.
Some Primitives & Types: top := fun (h:heap) => True. (* lift a pure term x into the computation space *) ret : (x:A),ST top A ( r i f => r=x f=i). (* allocate a fresh location *) new : (v:A),ST top loc ( r i f => i r = None f = update r v i). Note: leaving off implicit parameters.
More Primitives ptsto(x:loc)(v:A)(h:heap) := h x = Some{A; v} ref(A:Set)(x:loc)(h:heap):= v:A,ptsto x v h. read : (A:Set)(x:loc), ST (ref A x) A ( r i f => f = i ptsto x r i) write : (x:loc)(v:A), ST ( i => B, ref B x i) unit ( r i f => f = update x v i)
Bind (* sequentially compose computations *) bind:(C1: ST P1 A1 Q1) (C2: x:A1, ST (P2 x) A2 (Q2 x)), ST ( i => P1 i (r h, Q1 r i h -> P2 r h)) A2 ( r i f => x h, Q1 x i h Q2 x r h f)
Back to our Example: Definition postinc x := bind (read x) ( xv => bind (write x (xv + 1)) ( _ => ret xv)). Check postinc.
A Little Hard to Read… postinc : forall x : loc, ST (fun i : heap => (fun i0 : heap => exists v : nat, ptsto x v i0) i /\ (forall (x0 : nat) (m : heap), (fun (y : nat) (i0 m0 : heap) => m0 = i0 /\ ptsto x y i0) x0 i m -> (fun (xv : nat) (i0 : heap) => (fun i1 : heap => exists B : Type, exists w : B, ptsto x w i1) i0 /\ (forall (x1 : unit) (m0 : heap), (fun (_ : unit) (i1 m1 : heap) => m1 = update x (xv + 1) i1) x1 i0 m0 -> (fun (_ : unit) (_ : heap) => True) x1 m0)) x0 m)) (fun (y : nat) (i m : heap) => exists x0 : nat, exists h : heap, (fun (y0 : nat) (i0 m0 : heap) => m0 = i0 /\ ptsto x y0 i0) x0 i h /\ (fun (xv y0 : nat) (i0 m0 : heap) => exists x1 : unit, exists h0 : heap, (fun (_ : unit) (i1 m1 : heap) => m1 = update x (xv + 1) i1) x1 i0 h0 /\ (fun (_ : unit) (r : nat) (i1 f : heap) => r = xv /\ f = i1) x1 y0 h0 m0) x0 y h m)
Consequence: (* strengthen pre- and weaken post-condition *) do : (C : ST P1 A Q1), (i, P2 i -> P1 i x f, Q1 x i f -> Q2 x i f) -> ST P2 A Q2.
Re-Typing with Do Definition postinc x : ST (ref nat x) nat ( r i f => v, ptsto x v i -> r = v f = update x (v+1) i) := do (xv <-- !!x ; x ::= (xv + 1) ;; ret xv) <proof-term>.
Alternatively Program Definition postinc x : ST (ref nat x) nat ( r i f => v, ptsto x v i -> r = v f = update x (v+1) i) := do (xv <-- !!x ; x ::= (xv + 1) ;; ret xv) _. <proof-script>
Or Even: Program Definition postinc x : ST (ref nat x) nat ( _ _ => ref nat x) := do (xv <-- !!x ; x ::= (xv + 1) ;; ret xv) _.
Proof Holes • Program lets us leave holes (“_”) for proofs. • After the definition, we enter the proof script for each hole. • Can register default tactics which take care of easy proofs, or simplify goals. • In the previous examples, tactics manage to construct proofs automatically. • In bigger examples, at least simplify VCs to something that’s manageable. • Note, no need to specify pre/post-conditions if you don’t care.
Exceptions exn : Set. Inductive ans(A:Set) := | Val : A -> ans A | Exn : exn -> ans A. ST(P:pre)(A:Set)(Q:post(ans A)) : Set
Two Ways to Return: ret : (x:A), ST top A ( a i f => a=(Val x) f=i). throw : (A:Set)(e:exn), ST top A ( a i f => a=(Exn e) f=i).
Rebind: bind:(C1: ST P1 A1 Q1) (C2: x:A1, ST (P2 x) A2 (Q2 x)), ST ( i => P1 i (r m, Q1 (Val r) i m -> P2 r m)) A2 ( a i f => (x h, Q1 (Val x) i h Q2 x a h f) (e, a = (Exn e) Q1 a i f)
Catch is Similar: catch:(C1: ST P1 A Q1) (C2: e:exn, ST (P2 e) A (Q2 x)), ST ( i => P1 i (e m, Q1 (Exn e) i m -> P2 e m)) A ( a i f => (e h, Q1 (Exn e) i h Q2 e a h f) (x, a = (Val x) Q1 a i f)
Can code Conditionals: Program Definition If(test : {R} + {S}) (C1 : R -> ST P1 A Q1) (C2 : S -> ST P2 A Q2) : ST (fun i => (R -> P1 i) (S -> P2 i)) A (fun r i f => (R Q1 r i f) (S Q2 r i f)) := match test with | left pfR => do (C1 pfR) _ | right pfS => do (C2 pfS) _ end.
Arrays: Treated as an n-vector of (distinct) locations. vsub : (a:array n)(j:nat)(pf: j<n), loc. (* subscript on array a at offset j *) sub : (A:Set)(a:array n)(j:nat)(pf: j<n), ST (ref A (vsub a j pf)) A ( r i f => f = i ptsto (vsub a j pf) r i)).
Checked Subscript: lessthan : (n m:nat), {n<m}+{n>=m}. Definition csub(A:Set)(a:array n)(j:nat):= If (lessthan j n) (fun pfj => sub a j pfj) (fun _ => throw A IndexOutOfBounds).
A Bigger Example: Hashtables • Representation: array of list(key*value). • table := {len:nat ; arr:array len} • Abstraction: (key*value) sets. • kvset : key -> value -> Prop • reps(S:kvset)(t:table)(h:heap): Prop • Operations to create, insert, and lookup.
Type of Create: create(n:nat) : ST top table ( y i f => (* old values in memory are preserved *) r A (v:A),ptsto r v i -> ptsto r v f (* array locations are fresh *) j (p:j<len t), unallocated(vsub (arr t) j pj) i (* returns a table that represents {} *) t:table,y=Val t reps empty_set t f).
Type of Insert: insert : forall (k:key)(v:value)(t:table)(S:kvset), ST (reps S t) ( y i f => (* returns unit *) y=Val tt (* only changes location at (hash k) mod n *) ( r w,(r <> vsub (arr t)((hash k) mod (len t))) -> ptsto r w i -> ptsto r w f) (* array now represents {(k,v)}+S *) reps (insert_kvset (k,v) S) t f).
Type of Lookup lookup : (k:key)(t:table)(S:kvset), ST (reps S t) value (y i f => (* memory is unchanged *) f=i (* throws Lookup when key isn’t in S *) (v,~(S k v) y=Exn Lookup (* or else returns some v s.t. S k v *) v, y=Val v S k v))
Roadmap ST(P:pre)(A:Set)(Q:post A) : Set • Heaps, pre- & post-conditions • Basic monadic & state constructs • Exceptions, conditionals, arrays • Example: hashtable • Recursion and meta-theoretic issues • Modularity & separation • Current & future work
Recursion (CBV): ffix : (f : (body:x:A, ST(P x)(B x)(Q x)) (x:A), ST (P x)(B x)(Q x)), (x:A), ST (P x)(B x)(Q x).
For Example: Fixpoint fact(n:nat) {struct n} : nat := match n with 0 => ret 1 | S m => n * fact(m) end. Definition spec := n:nat, ST top nat (fun y i f => f=i y=Val(fact n)). Program Definition fact_body(fact:spec):spec := fun n => do (If (eqnat n 0) (fun _ => ret 1) (fun _ => p <-- fact (n-1) ; ret (n*p))) _. Definition fact_rec := ffix fact_body.
Defining ST in Coq? Can try to define: ST P A Q := {i:heap|P h} -> {f:heap; x:A |Q x i h} but then you sacrifice: • recursive (diverging) computations • non-deterministic computations • code that stores computations in the heap So we will do something different.
Consistency • No equations on computations! • (except for monad laws) • no way to run them, even in the logic. • Trivial model: ST = unit. • silly, but ensures we haven’t broken Coq. • Intermediate model: • ST = predicate transformers (see Sylvain’s talk) • not big enough -- can’t store ST’s in heap! • Denotational model: • Lars Birkedal & Rasmus Peterson • (subset of Coq corresponding to HTT)
Operational Soundness • For HTT subset [ESOP’07]: • give a fairly standard, small-step operational semantics • assume “heap-consistency” of logic • we get this with the trivial model • proved preservation & progress • Not an everyday type-soundness proof. • fact_rec really computes factorial. • hope: scales to things like concurrency…
Modularity Thus far, a “big footprint” approach. • specify changes to entire heap. • not modular. • inconvenient specifications. • real nightmare for concurrency. • but meta-theory much simpler. So we define a separation-based monad in terms of the big footprint ST. • STsep P A Q. • similar to ST, but “tight” footprint.
STsep Definition STsep(P:pre)(A:Set)(Q: post A):Set := ST (P*top) A (fun x => P -o Q x). where: top h := True. (P1*P2) h := h1 h2, h=h1h2P1h1P2h2. (this h1) h2 := h1=h2. (Q-P) if := i1f1h, i=i1h f=f1h Q i1 f1 P h. (P-oQ)if := h,(P*(this h))i ->(Q-(this h)) i f. Intuitively, when run in a heap P*(this h) for some h, yields a heap satisfying Q*(this h).
Separating Specs emp h := h = empty. (xv) h := h = update x v empty. definition snew(A:Set)(v:A): STsep emp A ( a i f => a v) := fun A v => do (new v) _. definition sread(A:Set)(x:loc): STsep ( i => v:A, (x v) i) A ( a i f => (x a) i f=i) := fun A x => do (read x) _.
Separating Bind Program Definition sbind (C1 : STsep P1 A1 Q1) (C2 : x:A1, STsep (P2 x) A2 (Q2 x)) : STsep (i => (P1 * top) i x h, (P1 -o Q1 x) i h -> (P2 x * top) h) A2 (y i f => x h,(P1 -o Q1 x) i h (P2 x -o Q2 x y) h f) := do (bind C1 C2) _.
Separating Do Program Definition sdo sdo(C : STsep P1 A Q1) (pf : i, P2 i -> h, (P1 * (this h)) i (y f, (Q1 y) - (this h)) i m -> Q2 y i f)), STSep P2 A Q2 := do C _.
Abstraction Record FinMap(key value:Set)(hash:key->nat):= { t : Set ; reps : set(key*value)->t->heap->Prop ; create : All n:nat, STsep emp t ( r _ => reps empty_set r) ; insert : All k v t S, STsep (reps S t) unit ( _ _ => reps (ins (k,v) S) t) ; ... } Can build hash table, association list, splay tree, etc. that meets this interface.
What’s Next? • Examples: • have simple data structures (e.g., hash tables.) • can abstract over internal invariants. • need larger libraries & apps to drive development of abstractions & tactics. • Extraction: • can extract Haskell code now. • but want to build a good ML extractor • no need to pay the overhead of laziness • can leverage effects in compiler
Scaling the Language • I/O and Foreign Functions • obviously needed for practical programming • perhaps a simple treatment of “worlds” • right sort of monad transformers? • Concurrency • looking at transactions (a la STM) as basis • continue to use STsep for sequential code • simplifies O’Hearn-style separation treatment of concurrency • but of course, models & soundness tricky
Concurrency: STsep P A Q -- sequential, undoable Cmd I P A Q -- parallel, not undoable par : Cmd I P1 A1 Q1 -> Cmd I P2 A2 Q2 -> Cmd I (P1*P2) (A1*A2) (Q1*Q2) atomic : STsep (I*P) A (I*Q) -> Cmd I P A Q