CS221/321 Lecture 9, Nov 16, 2010 5. More types ------------- The type language of TFun is minimal, but a real language needs more ways of building structured data. We will incrementally enrich the type language (and add corresponding new expression forms) by adding these features: (1) Products: pairs, or more generally n-tuples and records. (2) Sums, or disjoint tagged unions. (3) Recursive types, needed for lists and trees. (4) Polymorphic types, needed for "generalized" or "generic" functions. We can illustrate this by trying to enrich the language enough to be able to define the analogue of the ML list type constructor, which is a datatype defined by: datatype 'a list = nil | :: of 'a * 'a list To accomplish this, we will need products, sums, recursion, and polymorphism. ---------------------------------------------------------------------- 5.1 Products ------------ We add one new kind of type expression τ ::= ... | τ1 * τ2 A type like Int * Bool represents the set of ordered pairs containing an integer and a boolean. It represents the Cartesian product of the sets of values represented by Int and Bool. For sets, the Cartesion product is defined by: A × B = {(a,b) | a ∈ A, b ∈ B} To express such pairs, the expression language can be enriched with a pair expression: e ::= ... | (e1,e2) This allows us to build (construct) pairs, but to make use of pairs we need to be able to take them apart. This is provided by two projection operations, fst and snd: e ::= ... | (e1,e2) | fst e | snd e which are intuitively defined by the equations: fst (x,y) = x snd (x,y) = y (fst x, snd x) = x Pairs introduce a new form of syntactic value v ::= ... | (v1,v2) The small step reduction rules for these new expression forms are (as additions to those of Fig. 4.5): e1 ↦ e1' (10) -------------------- (e1,e2) ↦ (e1',e2) e2 ↦ e2' (11) -------------------- (v1,e2) ↦ (v1,e2') e ↦ e' (12) ---------------- fst e ↦ fst e' e ↦ e' (13) ---------------- snd e ↦ snd e' (14) ------------------ fst (v1,v2) ↦ v1 (14) ------------------ snd (v1,v2) ↦ v2 Note that there is no redex reduction rule for pair expressions. This corresponds to the fact that a pair of values is itself a value -- no further simplification is possible. The new typing rules for pairs and projections are (extending Fig 4.3): Γ ⊦ e1 : τ1 Γ ⊦ e2 : τ2 (7) ---------------------------- Γ ⊦ (e1,e2) : τ1 * τ2 Γ ⊦ e : τ1 * τ2 (8) ------------------ Γ ⊦ fst e : τ1 Γ ⊦ e : τ1 * τ2 (9) ------------------ Γ ⊦ snd e : τ2 ---------------------------------------------------------------------- Question: Why don't we just have fst and snd be new primitive operations, the way we now treat Plus and Eq? The problem is that primitives like Plus and Eq have fixed, definite types (Int → Int → Int and Int → Int → Bool respectively). But to conform to typing rules (8) and (9) fst and snd would need to have types fst : τ1 * τ2 → τ1 snd : τ1 * τ2 → τ2 for all types τ1 and τ2, and we don't yet have a way to specify these families of types (or type "schemes") in the type language. This will come when we add polymorphic types. Given the new transition rules and typing rules specified above, we can add the necessary new cases to the Preservation and Progress Lemmas. ---------------------------------------------------------------------- Examples x = (2,True) : Int * Bool fst x : Int ==> 2 snd x : Int ==> True (Plus, (False,(3,4))) : (Int * Int → Int) * (Bool * (Int * Int)) Nested tuples are not "associative": The nested pairs (3, (True, 2)) : Int * (Bool * Int) ((3, True), 2) : (Int * Bool) * Int are distinct values, of different, incompatible types. ---------------------------------------------------------------------- Exercise: Extend the proofs of Preservation and Progress to deal with the new primitives for product types. Exercise: Give big-step semantic rules for products and projections. Exercise: Give contextual reduction rules for pair and projection expressions. Exercise: Give CK and CEK rules for pair and projection expressions. Exercise: Prove that the extended small step semantics with pairs and projections is still deterministic. ---------------------------------------------------------------------- With products added to the language, it is possible to simplify the typing of the binary primitive operators: Plus, Times, Minus : Int * Int → Int Eq, Lt, Gt : Int * Int → Bool and then applications of primitives are simplified to: App(Plus, (e1,e2)) App(Eq, (e1,e2)) Generalizations: --------------- Pairs give us 2-tuples, two-element compound values. What about triples, quadruples, etc? We could treat these simply as nested pairs, e.g., (2, True, 3) = (2, (True, 3)) : Int * (Bool * Int) where the lhs is viewed as an abbreviation or syntactic sugar representing the rhs. This in fact is how n-tuples were constructed in the original version of ML. In SML, we have a different, n-argument type constructor for n-tuples, for each n >= 2. We could call these type constructors Tup2, Tup3, Tup4 and so on, and then the infix product operator * would be viewed as syntactic sugar for types constructed with these Tup[n] operators: Int * Bool == Tup2(Int,Bool) Int * Bool * Int == Tup3(Int,Bool,Int) In this scheme, flat n-tuples are of a different type from nested n-tuples: Int * Bool * Int =/= Int * (Bool * Int) Tup3(Int,Bool,Int) =/= Tup2(Int, Tup2(Bool,Int)) Another generalization is "records". These are products where the factors in the products are identified not by position but by a name or "label" (typically an identifier). {a = 3, b = True} : {a: Int, b: Bool} Here the order of the fields is not important, so {a: Int, b: Bool} == {b: Bool, a: Int}, and only the number and names of the field labels matters. Records have the advantage of being more "self-documenting" if the label names are well-chosen. E.g. {firstName : string, lastName: string, age: int} Record types can also be viewed as the result of applying a type constructor to arguments: {a: Int, b: Bool} === Rec{a,b} (Int,Bool) (the order of the arguments is determined by a canonical sorting of the set of labels -- here a comes before b, so Int is associated with a). ====================================================================== 5.2 Sum Types ------------- The sum of two types τ1 and τ2 contains values in τ1 and in τ2, but tagged to distinguish which of these they belong to (even if τ1 and τ2 are the same type!). This corresponds to the set-theoretic disjoint sum: A + B = {(0,a) | a ∈ A} ⋃ {(1,b) | b ∈ B} (arbitrarily using 0 and 1 as distinguishing tags). A complete set of basic operations for working with such tagged unions would include injection operations, projection operations, and descriminator operations: Injections inl : A → A + B, inl(a) = (0,a) inr : B → A + B, inr(b) = (1,b) Projections outl : A + B → A, outl (0,a) = a; outl (1,b) undefined outr : A + B → B, outr (1,b) = b; outl (0,a) undefined Descriminators isl : A + B → Bool, isl (0,a) = True; isl (1,b) = False isr : A + B → Bool, isr (0,a) = False; isl (1,b) = True So we could add all six of these as primitive operators in TFun. But a somewhat neater approach is to combine the functions of descrimination and projection into one new expression form, a case expression: case e of f; g where e should evaluate to an element of a sum type τ1 + τ2, and f and g are expressions denoting functions of type τ1 → τ and τ2 → τ, respectively. ---------------------------------------------------------------------- New abstract syntax: ---------------------------------------------------------------------- τ ::= ... | τ1 + τ2 e ::= ... | Inl e | Inr e | Case(e,f,g) v ::= ... | Inl v | Inr v ---------------------------------------------------------------------- ---------------------------------------------------------------------- Evaluation (TFun[SSv]) ---------------------------------------------------------------------- e ↦ e' (15) ---------------- Inl e ↦ Inl e' e ↦ e' (16) ---------------- Inr e ↦ Inr e' (17) -------------------------------- Case(Inl v,e2,e3) ↦ App(e2, v) (18) -------------------------------- Case(Inr v,e2,e3) ↦ App(e3, v) e1 ↦ e1' (19) ---------------------------------- Case(e1,e2,e3) ↦ Case(e1',e2,e3) ---------------------------------------------------------------------- ---------------------------------------------------------------------- Typing (TFun(typ)) ---------------------------------------------------------------------- Γ ⊦ e1 : τ1 (10) ----------------------- Γ ⊦ Inl e1 : τ1 + τ2 Γ ⊦ e2 : τ2 (11) ----------------------- Γ ⊦ Inr e2 : τ1 + τ2 Γ ⊦ e: τ1 + τ2 Γ ⊦ f: τ1 → τ Γ ⊦ g: τ2 → τ (12) ----------------------------------------------------- Γ ⊦ Case(e,f,g) : τ ---------------------------------------------------------------------- This treatment of Case is somewhat more general that we usually need. It will almost always be the case that the two case functions given by e2 and e3 in Case(e1,e2,e3) will be explicit lambda abstractions. E.g. Case(e1, (fn x => eleft), (fn y => eright)) so we could have a modifed abstract syntax constructor that reflects this fact: Case*(e1,x,eleft,y,eright) This binds the two variables x and y, with respective scopes eleft and eright. A typical concrete syntax for this kind of case expression would be (as in ML): case e1 of Inl x => eleft | Inr y => eright The corresponding evaluation and typing rules would be (17*) --------------------------------- Case*(Inl v,x,e2,y,e3) ↦ [v/x]e2 (18*) -------------------------------- Case*(Inr v,x,e2,y,e3) ↦ [v/y]e3 Γ ⊦ e: τ1 + τ2 Γ[x:τ1] ⊦ l: τ Γ[y:τ2] ⊦ r: τ (12*) -------------------------------------------------- Γ ⊦ Case*(e,x,l,y,r) : τ There is a problem with injection expressions like Inl e. How do we compute their types? This expression will have a type of the form τ1 + τ2, where we can get τ1 by computing the type of e, but where do we get τ2? We need to make the return types of Inl and Inr explicit in each construction, and we do this by passing those types as "type parameters" when we apply Inl and Inr. E.g. Inl[τ1,τ2](e) : τ1 + τ2, assuming e : τ1 Inr[τ1,τ2](e) : τ1 + τ2, assuming e : τ2 Thus Inl[Int,Bool] 3 : Int+Bool Inr[Int,Bool] True : Int+Bool The modified typing rules for Inl and Inr are: Γ ⊦ e1 : τ1 (10') ---------------------------- Γ ⊦ Inl[τ1,τ2] e1 : τ1 + τ2 Γ ⊦ e2 : τ2 (11') ---------------------------- Γ ⊦ Inr[τ1,τ2] e2 : τ1 + τ2 ---------------------------------------------------------------------- From now on, we will consider Case* to be the "official" form for the case construct, and will therefore drop the * and call it simply Case. Case(e1,x,e2,y,e3) is the abstract syntax form, which will be written case e1 of Inl x => e2 | Inr y => e3 in concrete syntax. Examples -------- (1): Int + Bool x : Int + Bool = Inl[Int,Bool] 2 y : Int + Bool = Inr[Int,Bool] True (case x of Inl u => u + 3 | Inr v => 0) : Int ==> 5 (case y of Inl u => u + 3 | Inr v => 0) : Int ==> 0 (2) Bool as a sum type. let Bool' = Unit + Unit True' = Inl[Unit,Unit] () False' = Inl[Unit,Unit] () A conditional form If' could be introduced as syntactic sugar for a case expression over Bool': If'(e1,e2,e3) === case e1 of Inl x => e2 | Inr y => e3 where it is assumed that x does not occur free in e2, nor y in e3. ====================================================================== 5.3 Recursion ------------- Equirecursion vs Isorecursion ----------------------------- (1) Equirecursion The recursive type expression μt.τ is supposed to represent a "fixed-point" of the type operator λt.τ. Thus we should be able to unwind (or "unfold") a recursive type expression as follows: μt.τ == (λt.τ)(μt.τ) == [μt.τ/t]τ and so these two types should be "equal": μt.τ == [μt.τ/t]τ Let's look at a particular example: Let τ = μt.(Int * t). We have τ = Int * τ This can be repeatedly unwound as follows: μt.(Int * t) = Int * τ = Int * (Int * τ) = Int * (Int * (Int * τ)) = Int * (Int * (Int * (Int * τ))) ... This unwinding process, if carried on "forever", gives us an infinite type expression of the form τ = Int * (Int * (Int * (Int * (Int * ... so τ can be thought of as equivalent to an infinite type expression. (Lets not worry here about how to define values having type τ.) So how do we work with type expressions that are essentially infinite? Consider the following two terms: τ1 = μt.t * t τ2 = μt.t * (t * t) by unfolding these a few times, it becomes clear that they both have the same infinitely unwound form, which looks like an infinite binary tree with the product operator * at each node: * / \ / \ / \ * * / \ / \ / \ / \ * * * * ... ..... ... Since these two terms have the same infinite unfolding, we should consider them to be "equal". Thus with μt.τ terms, the equality relation on type terms becomes "interesting", and it will not be "obvious" in general whether to complicated recursive type terms will turn out to be equal. This is a bad feature in terms of a programmers working model of the type system. (2) Isorecursion To simplify the type equality problem when we introduce recursive types, we can prevent the free unfolding of recursive definitions. We do this by introducing two new primities that together can be thought of as defining an isomorphism between a recursive type and its unfolding: Unfold : μt.τ → [μt.τ/t]τ Fold : [μt.τ/t]τ → μt.τ Then when we want to go from a recursive type to its unfolding, or vice versa, we need to explicitly enable the type transfer using the Fold and Unfold operations. These operations are "generic", so we have to treat them as new syntactic expression forms (as we did with Fst, Snd, Inl, Inr). ---------------------------------------------------------------------- Syntax: τ ::= ... | t | μt.τ e ::= ... | Fold e | Unfold e v ::= ... | Fold v ---------------------------------------------------------------------- ---------------------------------------------------------------------- Evaluation: e ↦ e' (20) ------------------- Fold e ↦ Fold e' e ↦ e' (21) ---------------------- Unfold e ↦ Unfold e' (22) -------------------- Ufold(Fold v) ↦ v ---------------------------------------------------------------------- ---------------------------------------------------------------------- Typing: Γ ⊦ e : [μt.τ/t]τ (12) -------------------- Γ ⊦ Fold e : μt.τ Γ ⊦ e : μt.τ (13) -------------------------- Γ ⊦ Unfold e : [μt.τ/t]τ ---------------------------------------------------------------------- Example: integer lists ---------------------- Using recursive types, we can define a type of lists containing integers (generic lists will be defined later using polymorphism). We will use "Ilist" as the name of the integer list type: Ilist == μt. (Unit + Int * t) Think of this as a type that satisfies the fixpoint equation Ilist = Unit + Int * Ilist Here Unit is a new primitive type (like Int and Bool). It is a trivial type containing one value, the nil-tuple (): () : Unit Given this defn of Ilist, how do we define nil and cons operations for building integer lists (i.e. values of type Ilist): nil = Fold (Inl[Unit,Int*Ilist] ()) cons = λp : Int * Ilist . Fold (Inr[Unit,Int*Ilist] p) How about the null function that tests whether an Ilist is empty: null = λl : Ilist . case Unfold l of Inl x => True | Inr y => False Here is the length function for Ilists: letrec length = λl : Ilist . case Unfold l of Inl x => 0 | Inr y => 1 + length (Snd y) [Here I have had to restore recursive definitions to TFun using a letrec definition construct. We will discuss recursion in TFun below. ] letrec map = λf : Int → Int . λl: Ilist. case Unfold l of Inl x => nil | Inr y => Fold (Inr(f(Fst y),map f (Snd y))) ---------------------------------------------------------------------- Type checking with recursion ---------------------------- When typing an expression like Fold(e), we expect the type of e to be of the form [μt.τ/t]τ (Lecture 10, typing rule (12)]. But for an arbitrary type expression τ' computed for e, how do we determine the correct type τ such that τ' = [μt.τ/t]τ , and is this τ unique? For instance, suppose e = λx: μt.(t → t). x : μt.(t → t) → μt.(t → t) so the type of e is τ' = μt.(t → t) → μt.(t → t). If we want to type Fold(e), how do we construe τ' as having the form [μt.τ/t]τ? Well, it turns out there under the equi-recursive theory of type equality, where two recursive types are equal if the trees resulting from their infinite unfoldings are isomorphic, there are an infinite number of possible choices for μt.τ. The most obvious would be μt.(t → t), since [μt.(t → t)/t](t → t) = μt.(t → t) → μt.(t → t) = τ' But there are many other recursive type expressions denoting the same type! μt.(t → (t → t)) μt.((t → t) → t) μt.((t → t) → (t → t)) μt.(t → μt.(t → t)) μt.(μt.(t → t) → t) ... So the problem does not have a unique solution. We could try to resolve this ambiguity by choosing the "least" or "simplest" solution, which in this case would appear to be μt.(t → t), but it is not clear that in general there will be a unique simplest solution. But the whole point introducing iso-recursive types with Fold and Unfold coercions was to avoid the complications of determining type equality in the equi-recursive case! So we really want a simple and obvious solution. The "brute force" solution is simply to specify the desired result type in the Fold expression, similarly to how we handled the ambiguity inherent in the Inl and Inr injections for sum types. Thus we have to modify Fold to take a type argument, which should be the recursive type that we want the value to have. Γ ⊦ e : [μt.τ/t]τ (12) -------------------------- Γ ⊦ Fold[μt.τ] e : μt.τ Now the determine of the type of Fold[μt.τ] (e) becomes trivial, and the remaining problem is to check that the type of e is equal to [μt.τ/t]τ, for the explicitly specified μt.τ. Note that we don't have to do this for Unfold, where we can still use the original typing rule (13), because the recursive type is determined by typing e in Unfold(e), and we just need to unfold that to get the type of Unfold(e). All the recursive types that we will encounter during type checking will be explicitly introduced as type arguments to Fold, so we won't have to "invent" any recursive types. This gets to be a bit cumbersome in practice. For instance, for Ilist the definition of nil becomes nil = Fold[μt. (Unit + Int * t)] (Inl[Unit,Int*(μt. (Unit + Int * t))] ()) and you can imagine how much messier it gets when the recursive definitions get more complicated, or mutually recursive types are introduced. If we allow type definitions like type Ilist == μt. (Unit + Int * t) to introduce names for recursive types, the definition of nil becomes more concise: nil = Fold[Ilist] (Inl[Unit,Int*Ilist] ()) But if we allow such definitions, we must take them into account when computing equality of type expressions. Specifically, we need to "interpret" such names as we compare two type expressions. Computing type equality will then have to be done in the context of a type environment that maps type names to their definitions. [Note: There is another complication for type equality if we want to have alpha-equivalence of type expressions containing μ-bound type variables: e.g. μt.(t → t) == μs.(s → s). The algorithm for equality modulo change of bound variables is quite a bit more complicated than that for simple type expressions without bound variables, where ML's built-in structural equality suffices. For the purposes of Homework 5, we'll ignore this issue.] ====================================================================== 5.4 Polymorphism ---------------- To define generic operations (and other values) that have multiple types, we introduce "parametric polymorphism". This features allows us to abstract an expression with respect to a type variable, creating a polymorphic value, and then instantiate that value at various types. ---------------------------------------------------------------------- Syntax: τ ::= ... | ∀t.τ Poly(t,τ) e ::= ... | Λt.e | e[τ] TFun(t,e) | TApp(e,τ) v ::= ... | Λt.e ---------------------------------------------------------------------- Polymorphic abstractions are considered to be values, so type abstraction suspends evaluation of the body expression. Type applications are reduced by the type analogue of β-reduction: the type argument is substituted for the type parameter in the body expression. As usual, we also have a search rule for evaluating the rator of a TApp, but we assume the argument type expression does not require evaluation, so type application is effectively CBN. ---------------------------------------------------------------------- Evaluation: e ↦ e' (23) ----------------------- TApp(e,τ) ↦ TApp(e',τ) (24) -------------------------- TApp(TFun(t,e),τ) ↦ [τ/t]e ---------------------------------------------------------------------- For typing, we need to deal with a novel issue. The Λ-abstraction construct introduces a bound type variable, and body expression of the Λ-abstraction is the scope of that type variable. Thus in Λt.e, type variable t can appear free in type expressions within e. During type checking, we need to determine which type variables are currently "in scope", i.e. were introduced by Λ-abstractions in the expression context of the current expression. To do this, we add a new context component, Δ, to the typing judgement: Γ; Δ ⊦ e : τ Δ is a set of type variables that are in scope at this point, and which are allowed to appear free in τ and in type expressions inside e. There is an auxiliarly judgement stating that a type is relatively closed with respect to a variable set Δ: Δ ⊦ τ : Type This means that τ is a well-formed type, all of whose free variables are in the set Δ. This is analagous to the Γ ⊦ e ok judgement that we defined for the SEAL language, as a precursor of type checking. ---------------------------------------------------------------------- Typing: Γ,Δ⋃{t} ⊦ e : τ Δ ⊦ τ : Type (14) -------------------------------------- Γ,Δ ⊦ Λt.e : ∀t.τ Γ,Δ ⊦ e : ∀t.τ Δ ⊦ ∀t.τ : Type Δ ⊦ τ1 : Type (15) --------------------------------------------------- Γ,Δ ⊦ e[τ1] : [τ1/t]τ ---------------------------------------------------------------------- Examples: (1) polymorphic identity Id = Λt. λx: t. x Id[Int] ↦ λx: Int. x : Int → Int Id[Int] 3 ↦ (λx: Int. x) 3 → 3 (2) polymorphic fst and snd fst : ∀s.∀t.(s * t) → s snd : ∀s.∀t.(s * t) → t fst(1,true) === fst[Int][Bool](1,true) ↦ 1 (3) polymorphic Inl and Inr Inl : ∀s. ∀t. s → s+t Inr : ∀s. ∀t. t → s+t (4) generic lists List : Type => Type (the "kind" of the list operator) List = λt. μs. (Unit + t * s) nil : ∀t.List(t) = Λt.Fold(Inl[Unit,t*List(t)] ()) cons : ∀t.t*List(t) → List(t) = Λt.λp : t * List(t). Fold (Inr[Unit,t*List(t)] p) ---------------------------------------------------------------------- Note: Example (4) defines a list type function that defines a new recursive type for each argument type. E.g. List(Int) = μs. (Unit + Int * s) List(Bool) = μs. (Unit + Bool * s) List(Int → Bool) = μs. (Unit + (Int → Bool) * s) It is possible, however, to define list as a recursive type function: list = μL: Type=>Type . λt. (Unit + t * L(t)) Here the recursion is using the μ-operator a the higher kind (Type => Type) rather than at kind Type. ====================================================================== 5A. Appendix: Unified Definitions of PTFun ------------------------------------------ 5A.1 Syntax ----------- We start with the "informal" concrete syntax that we can use for writing expressions in PTfun. This is not a grammar that is designed for parsing, and it doesn't necessarily define the syntax completely and unambiguously (e.g. nothing is said about infix operator symbols, precedence, etc.). ---------------------------------------------------------------------- Fig. 5.1: PTFun "Concrete" Syntax ---------------------------------------------------------------------- x ∈ var ::= x, y, z, ... (alphanumeric variables + symbolic variables) n ∈ num ::= 0, 1, 2, ... (natural numbers or integers) b ∈ bool ::= true, false t ∈ tyvar ::= s,t,u,v,... (type variables) τ ∈ tyexpr ::= t | Unit | Int | Bool | τ1 → τ2 | τ1 * τ2 | τ1 + τ2 | μt.τ | ∀t.τ e ∈ expr ::= x | n | b | () | if e1 then e2 else e3 | let x = e1 in e2 | letrec x: τ1→τ2 = λx1.e1 in e2 | λx:τ.e | e1 e2 | (e1,e2) | "case e of Inl x => e1 | Inr y => e2" | Λt.e | e[τ] ---------------------------------------------------------------------- Here is the corresponding abstract syntax, which can easily be translated into SML datatypes. Note that the components of the function expression for the definiens in the letrec are broken our as direct components of the letrec. ---------------------------------------------------------------------- Fig. 5.2: PTFun Abstract Syntax ---------------------------------------------------------------------- x ∈ var ::= x, y, z, ... (alphanumeric variables + symbolic variables) n ∈ num ::= 0, 1, 2, ... (natural numbers or integers) b ∈ bool ::= true, false t ∈ tyvar ::= s,t,u,v,... (type variables) τ ∈ tyexpr ::= VARty(t) | UNITty | INTty | BOOLty | FUNty(τ1,τ2) | PRODty(τ1,τ2) | SUMty(τ1,τ2) | RECty(t,τ) | POLYty(t,τ) e ∈ expr ::= Var(x) | Num(n) | Bool(b) | Unit -- atomic expressions If(e1, e2, e3) | Let(x, e1, e2) | Letrec(x, τ1, τ2, x1, e1, e2) | Fun(v, τ, e) | App(e1, e2) | Pair(e1, e2) | Case(e, v1, e1, v2, e2) | TFun(t,e) | TApp(e,τ) ---------------------------------------------------------------------- For the big-step semantics with environments, we can treat primitive operator names as predefined variables bound in an initial environment to values representing primitive operators. 5A.2 PTFun[BSv] Big-step semantics with environments ---------------------------------------------------- We need to define the set of (expressible/denotable) values, which for the big-step semantics are not fully captured within the abstract syntax (because of function closures, with their environment component). Environments map variables to values, and are defined recursively with values. ---------------------------------------------------------------------- Figure 5.3: TFun values and environments ---------------------------------------------------------------------- v ∈ value v ::= UNIT | NUM(n) | BOOL(b) | -- constants PRIM(value → value) | -- primitive operators INL | INR | -- sum constructors PAIR(v1,v2) | -- pair constructor FUN(x,e,env) | -- function closure TFUN(e,env) -- polymorphic closure env = variable → value ---------------------------------------------------------------------- We also need to define an initial environment containing bindings of all the primitive operations of the language. These include the arithmetic and relational operators, as well as the primitive projectors Fst and Snd for products, Inl and Inr constructors for sums. This environment is called Eprim and is defined in Fig. 5.6. ---------------------------------------------------------------------- Figure 5.4: Eprim. Primitive env for PTFun[BSv] ---------------------------------------------------------------------- Eprim(plus) = PRIM(λv.case v of PAIR(NUM n1,NUM n2) => n1+n2 | _ => error) ... (and so on for other arithmetic and relational operators) Eprim(Fst) = PRIM(λv.case v of PAIR(v1,v2) => v1 | _ => error) Eprim(Snd) = PRIM(λv.case v of PAIR(v1,v2) => v2 | _ => error) Eprim(Inl) = PRIM(λv.INL(v)) Eprim(Inr) = PRIM(λv.INR(v)) ---------------------------------------------------------------------- We define the big-step semantics in terms of a ternary relation Eval(e,E,v) relating an expression e, the environment E in which it is evaluated, and its value v. We could prove that this is a single-valued relation (at most one value v for given e and E), and this would justify defining an eval function by eval(e,E) = v iff Eval(e,E,v) ---------------------------------------------------------------------- Fig. 5.5: PTFun[BSv] -- PTFun Big-Step, CBV, Env-based evaluation ---------------------------------------------------------------------- Eval ⊆ expr * env * value (1) Eval(Var(x),E,E(x)) (2) Eval(Unit,E,UNIT) (3) Eval(Num(n),E,NUM(n)) (4) Eval(Bool(b),E,BOOL(b)) Eval(e1,E,BOOL(true)) Eval(e2,E,v) (5.1) ---------------------------------------- eval(If(e1,e2,e3),E,v) Eval(e1,E,BOOL(false)) Eval(e3,E,v) (5.2) ---------------------------------------- eval(If(e1,e2,e3),E,v) Eval(e1,E,v1) Eval(e2, E[x=v1], v) (6) ---------------------------------------- Eval(Let(x,e1,e2),E,v) E1 = μE'.E[x=FUN(y,e1,E')] Eval(e2,E1,v) (7) --------------------------------------------- Eval(Letrec(x,τ1,τ2,y,e1,e2),E,v),E,v) (8) Eval(Fun(x,τ,e),E,FUN(x,e,E)) Eval(e1,E,FUN(x,e,E')) Eval(e2,E,v2) Eval(e,E'[x=v2],v) (9) -------------------------------------------------------------- Eval(App(e1,e2),E,v) Eval(e1,E,v1) Eval(e2,E,v2) (10) ---------------------------------- Eval(Pair(e1,e2),E,PAIR(v1,v2)) Eval(e,E,INL(v1)) Eval(e1,E[x=v1],v) (11.1) --------------------------------------- Eval(Case(e,x,e1,y,e2),E,v) Eval(e,E,INR(v1)) Eval(e2,E[y=v1],v) (11.2) --------------------------------------- Eval(Case(e,x,e1,y,e2),E,v) Eval(e,E,v) (12.1) ------------------------------ Eval(App(Fold,e),E,FOLD(e)) Eval(e,E,FOLD(v)) (12.2) ------------------------- Eval(App(Unfold,e),E,v) (13) Eval(TFun(t,e),E,TFUN(e,env)) Eval(e,E,TFUN(e1,E')) Eval(e1,E',v) (14) ----------------------------------------- Eval(TApp(e,τ),E,v) ---------------------------------------------------------------------- Notes: (1) In rule (7), for letrec, we take advantage of the fact that environments are metalevel functions and give a recursive definition of the environment E'. In the SML implementation, we use an association list representation of the environment, and instead of creating a recursive environment, we use a device involving a special RFUN closure used to represent the recursive function in the environment. These RFun bindings are treated specially at variable lookup to achieve the necessary recursive self-reference when evaluating the function body. ---------------------------------------------------------------------- 5A.3 Small-step Semantics of PTFun ---------------------------------- In the small-step semantics, with substitution, it is more conveninent to introduce another syntax category for primitive operator constants: p ∈ prim ::= plus, times, minus, eq, lt, gt, fst, snd, inl, inr, fold, unfold and a corresponding expr constructor: e ∈ expr ::= ... | Prim(p) | ... For the transitition semantics using substitution, it is also more convenient to view the Letrec construct as syntactic sugar for an ordinary let where the definiens is a μ-expression: letrec f:τ = λx.e1 in e2 ===> let f = μf:τ.λx.e1 in e2. We represent the μ expression μf:τ.e by Rec(f,τ,e) in an extension of the abstract syntax: e ∈ expr ::= ... | Rec(f,τ,e) | ... and we treat such μ-expressions as values. ---------------------------------------------------------------------- Figure 5.6: PTFun value expressions (for PTFun[SSv]) ---------------------------------------------------------------------- v ∈ value ::= Unit | Num(n) | Bool(b) | Prim(p) | Fun(x,τ,e) | Rec(x,τ,e) Pair(v1,v2) | App(Prim(inl),v) | App(Prim(inr),v) | App(Prim(fold),v) | TFun(t,e) ---------------------------------------------------------------------- ---------------------------------------------------------------------- Figure 5.7: PTFun[SSv] - CBV small-step sematics for PTFun ---------------------------------------------------------------------- transition: ↦ ⊆ expr * expr (1.1) App(Prim(p),v) = primApply(p,v) (p ∉ {inl,inr,fold}) (1.1) App(Fun(x,τ,e),v2) ↦ [v2/x]e (ordinary function application) (v1 = Rec(f,Fun(x,τ,e)) (1.3) ------------------------------ (recursive function application) App(v1, v2) ↦ [v1,v2/f,x]e e1 ↦ e1' (1.4) --------------------------- App(e1,e2) ↦ App(e1', e2) e2 ↦ e2' (1.5) ---------------------------- App(v1, e2) ↦ App(v1, e2') (2.1) If(True,e2,e3) ↦ e2 (2.2) If(False,e2,e3) ↦ e3 e1 ↦ e1' (2.3) ------------------------------- If(e1,e2,e3) ↦ If(e1',e2,e3) (3.1) Let(x,τ,v1,e2) ↦ [v1/x]e2 e1 ↦ e1' (3.2) ---------------------------------- Let(x,τ,e1,e2) ↦ let(x,τ,e1',e2) e1 ↦ e1' (4.1) ----------------------------- Pair(e1,e2) ↦ Pair(e1', e2) e2 ↦ e2' (4.2) ------------------------------ Pair(v1, e2) ↦ Pair(v1, e2') (5.1) Case(App(Prim(Inl),v),x,e1,y,e2) ↦ [v/x]e1 (5.2) Case(App(Prim(Inr),v),x,e1,y,e2) ↦ [v/y]e2 e ↦ e' (5.3) ---------------------------------------- Case(e,x,e1,y,e2) ↦ Case(e',x,e1,y,e2) (6.1) TApp(TFun(t,e),τ) ↦ e e ↦ e' (6.2) ------------------------ TApp(e,τ) ↦ TApp(e',τ) ---------------------------------------------------------------------- Notes: (1) Rule (1) only applies to those primitive operator applications that have not been categorized as value expressions in Fig. 5.3. (2) primApply(p,v) returns the value of binary primop bop on the argument v, and wraps the result value with the appropriate expression constructor if necessary. E.g. primApply(plus,Pair(Num n1,Num n2)) ==> Num(n1+n2) primApply(eq,Pair(Num n1,Num n2)) ==> Bool(n1=n2) primApply(fst,Pair(v1,v2)) ==> v1 primApply(snd,Pair(v1,v2)) ==> v2 primApply(unfold,App(Prim(fold),v)) ==> v ---------------------------------------------------------------------- 5A.4 Type checking for PTFun ---------------------------- ---------------------------------------------------------------------- Figure 5.8: TFun[Typ] - Rules for the typing judgement Γ;Δ ⊦ e: τ ---------------------------------------------------------------------- Rules: (1.1) Γ;Δ ⊦ Unit : UNITty (1.1) Γ;Δ ⊦ Num(n) : INTty (1.1) Γ;Δ ⊦ Bool(b) : BOOLty Γ(x) = τ (2) ---------------- Γ;Δ ⊦ Var x: τ Γ;Δ ⊦ e1: Bool Γ;Δ ⊦ e2: τ Γ;Δ ⊦ e3: τ (3) -------------------------------------------- Γ;Δ ⊦ If(e1,e2,e3): τ Γ;Δ ⊦ e1: τ Γ[x:τ];Δ ⊦ e2: τ' (4) ------------------------------------- Γ;Δ ⊦ Let(x,τ,e1,e2) : τ' Γ[y:τ1, x:τ1→τ2];Δ ⊦ e1: τ2 Γ[x:τ1→τ2];Δ ⊦ e2: τ (5) ----------------------------------------------------- Γ;Δ ⊦ Let(x,τ1,τ2,y,e1,e2) : τ Γ[x: τ1];Δ ⊦ e: τ2 (6) ----------------------------- Γ;Δ ⊦ Fun(x,τ1,e) : τ1 → τ2 Γ;Δ ⊦ e1: τ1 → τ2 Γ;Δ ⊦ e2: τ1 (7) ---------------------------------- Γ;Δ ⊦ App(e1,e2) : τ2 Γ;Δ ⊦ e1: τ1 Γ;Δ ⊦ e2: τ2 (8) ----------------------------- Γ;Δ ⊦ Pair(e1,e2) : τ1 * τ2 Γ;Δ ⊦ e: τ1+τ2 Γ[x:τ1];Δ ⊦ e1: τ Γ[y:τ2];Δ ⊦ e2: τ (9) -------------------------------------------------------- Γ;Δ ⊦ Case(e,x,e1,y,e2) : τ Γ;Δ ⊦ e: [μt.τ/t]τ (10) -------------------------- Γ;Δ ⊦ Fold[μt.τ]e: μt.τ Γ;Δ ⊦ e: μt.τ (11) ---------------------------- Γ;Δ ⊦ Unfold e: [μt.τ/t]τ Γ,Δ⋃{t} ⊦ e : τ (12) ------------------------- Γ,Δ ⊦ TFun(t,e) : ∀t.τ Γ,Δ ⊦ e : ∀t.τ (13) ---------------------------- Γ,Δ ⊦ TApp(e,τ1) : [τ1/t]τ ---------------------------------------------------------------------- Note that we do not have special rules for the application of the primitives Fst, Snd, Inl, Inr, but we do include special rules (10) and (11) for applications of Fold and Unfold. This is because we now have polymorphic types, so Fst, Snd, Inl, and Inr can be assigned polymorphic types in a primitive type environment, as described in Figure 5.9. To keep the rules simple, we have not included the relative closure judgements for the types that appear. If we add these, rule (9), for instance, would become: Δ ⊦ τ1 : Type Δ ⊦ τ2 : Type Δ ⊦ τ : Type Γ;Δ ⊦ e: τ1+τ2 Γ[x:τ1];Δ ⊦ e1: τ Γ[y:τ2];Δ ⊦ e2: τ (9) -------------------------------------------------------- Γ;Δ ⊦ Case(e,x,e1,y,e2) : τ Here I've "stacked" the premises in two rows (sometimes you will find complicated inference rules with many premises, stacked in as many as six or more rows). See the comment below in Implementation Notes regarding how to deal with the relative closure issue in type checkers. ---------------------------------------------------------------------- Figure 5.8: Γ0 - type environment for primities ---------------------------------------------------------------------- Γ0(plus) = Int * Int → Int (similarly for times, minus) Γ0(eq) = Int * Int → Bool (similarly for lt, gt) Γ0(Fst) = ∀t1,t2. t1 * t2 → t1 Γ0(Snd) = ∀t1,t2. t1 * t2 → t2 Γ0(Inl) = ∀t1,t2. t1 → t1 + t2 Γ0(Inr) = ∀t1,t2. t2 → t1 + t2 ----------------------------------------------------------------------- However, this does not work for Fold and Unfold, which are "polymorphic" in a sense, but in an ad hoc manner that relates a recursive type to its unwinding. Fold : [μt.τ/t]τ → μt.τ Unfold : μt.τ → [μt.τ/t]τ These types cannot be expressed as ordinary polymorphic types, and hence the need to treat Fold and Unfold as syntax constructors instead of primitive functions, and hence rules (10) and (11). But in the dynamic semantics described above, PTFun[BSv] and PTFun[SSv], we have taken some liberties and left out the required TApps and type arguments when dealing with Fst, Snd, Inl, and Inr. These TApps and type arguments do not affect the evaluation, however, so we can suppose that the extra type information has been "erased" before we perform the evaluations or reductions specified above. However, if we were being complete and precise, we would have to define the primite evaluations for Fst, etc. more carefully. For instance, in the small-step semantics, the definition of primApply for fst might become: primApply(TApp(TApp(Prim(Fst),τ1),τ2), Pair(v1,v2)) ==> v1 Alternatively, we could leave primApply as is, and define additional reduction rules to eliminate these TApps, such as: TApp(Prim(Fst),τ) ↦ Prim(Fst) Similarly, for the big-step semantics, we would need to add a rule like Eval(e,E,PRIM(f)) (15) ---------------------------- Eval(Tapp(e,τ),E,PRIM(f)) to eliminate the TApps around the polymorphic primitives. ---------------------------------------------------------------------- Implementation Issues --------------------- 1. The role of types in dynamic semantics. The types used in type annotations are ignored by the evaluation rules, and the same is true of the evaluation rules (or primitive definitions) for polymorphic primitives like Fst, Snd, Inl, Inr. We could define the evaluation semantics by first erasing all the type ascriptions, and type applications and their type arguments, but the one place where type type system affects evaluation is type abstraction (Λt) to create polymorphic values. We are treating such applications as creating syntactic values (suspensions, closures), and these have to be cancelled by type application. If we wanted to erase the types befor evaluation, we should replace TFun and TApp expressions by ordinary suspensions and applications: TFun(t,e) ==> Fun(x, UNITty, e) TApp(e,τ) ==> App(e, Unit) 2. How to deal with relative closure of types in type checking. A relative closure judgement like Δ ⊦ τ : Type can easily be implemented by computing the set of free type variables of τ and checking that it is a subset of Δ. But this would be rather expensive if we do this check for every type expression that occurs during type checking, and it would involve a lot of redundant checking of the same type expressions. We can avoid this work by ignoring the issue, which amounts to treating any free type variables as new type constants. But these type constants would have no rules for creating or eliminating associated values, and hence would appear to be useless. For example, consider e = λx:t.x where t is a free type variable. e is clearly a function, and it could be assigned the type e : t → t but how could one create an expression (or a value) that this function could be applied to? If we want to disallow the occurrence of free type variables, the best way to achieve this is to detect them when parsing and building the abstract syntax, rather than during type checking. ----------------------------------------------- Document History ----------------------------------------------- 12/2/2010, 12:00pm: * Added missing rule (12.2) in Fig 5.5 and renumbered last few rules in that figure. Correspondingly renumbered Rule (16) to (15) in last para. * Corrected typo in line defining eval in terms of Eval. * Corrected treatment of Fold and Unfold as predefined variables in Eprim. These still have to be treated as distinct expression constructors. 12/3/2010, 12:30pm: * Added discussion of relative closure judgements for types. * Corrected treatment of constants in Fig. 5.8. * Numbered subsections. * Added Implementation Issues section at end of Appendix 5A. * Corrected a couple minor typos.