Type Operators -------------- In this lecture, we continue to discuss type operators: function abstraction and application at type-level. For this, we'll present syntax, operational semantics, and typing rules for a language \lambda_\omega with type operators. The subtleties of the language \lambda_\omega come from two facts: (1) to guarantee the well-formedness of type expressions, we must introduce kinding, the types of types; and the accompanying kinding rules (just like typing rules for expressions); (2) to type check expressions, we must introduce some notion of equivalence between type expressions and the equivalence checking rules. Some languages allow programmers to define type-level operations, say, generics in Java or templates in C++). For instance, in Java, we can define the following Pair class with two instance variables: class Pair<X>{ X first; X second; } roughly, this class can be thought of a type-level function which maps any incoming class X to a record, that is: Pair = \Lambda X.{first: X, second: X} note the difference between type-level functions and polymorphic functions, which map type arguments to expressions. To apply the function Pair, we can supply it a type, as in: Pair<Integer> which is a concrete type. To formalize the type-level operations, we define the following language \lambda_\omega: v -> true | false | \lambda x:T.e e -> v | if (e, e, e) | e e T -> Bool | X | T->T | \Lambda X.T | T T It's very close to the syntax of the simply typed \lambda-calculus we discussed in chapter 9. But here are some key differenes: first, we have introduced more syntax constructs into the type definitions of T: besides boolean type "Bool" and function type "T->T", we also have type variable "X", the type-level abstraction "\Lambda X.T", and type-level application "T T". Thus, we can do full-fledged computations on types. Let's write down some sample types: (1) \Lambda X.X (2) (\Lambda X.X) Bool (3) Bool Bool (4) (\Lambda X.X) (\Lambda Y.Y) (5) (\Lambda X. \Lambda Y. X (X Y)) (\Lambda X.X) Bool It's worth remarking that as type expressions T is untyped (it's essentially the untyped lambda calculus via a one-to-one correspondence), so it exhibits all the problems from the untyped lambda calculus, especially, not all type expressions are well-formed (say the expression Bool Bool in (3): we can not apply a boolean type to another boolean type). For this reason, in the literature, the type expressions are often termed constructors. So our next job is to design a "type system" for constructors to rule out ill-formed constructors such as (3). However, in order to distinguish from the term-level type system, we'll call it a kind system. Here is the definition for possible kind K: K -> * | K=>K where the symbol * stands for the kind for any constructors that can inhabited by an expression e; and the second form of kind K1=>K2 specifies the constructor abstraction from a constructor of kind K1 to a constructor of kind K2. With the syntax of kind, we can give a judgment form D |- T::K to specify the kinding rules on constructors, and these kinding rules make use of an kinding environment D which maps a constructor variable X to its corresponding kind K: D -> . | X::K, D The kinding rules are syntax-directed: -------------------------------------------------(K-Bool) D |- Bool:: * X::K \in D -------------------------------------------------(K-TyVar) D |- X:: K D |- T1:: * D |- T2:: * -------------------------------------------------(K-Arrow) D |- T1->T2:: * D, X::K |- T::K' -------------------------------------------------(K-Abs) D |- \Lambda X::K.T:: K=>K' D |- T1:: K1=>K2 D |- T2:: K1 -------------------------------------------------(K-App) D |- T1 T2:: K2 It's worth remarking that, though at first glance these rules are similar to the typing rules from simply typed lambda calculus, but there are some subtle points. Especially, these rules imply a somewhat leveled structure on types. For instance, the above K-Arrow rule requires that both the constructors T1 and T2 should be of kind *, instead of arbitary kinds, so it rules out constructors like this: Bool -> \Lambda X::*.X The dynamic semantics for \lambda_\omega is somewhat standard: e1 -> e1' --------------------------------------------(E-If) if(e1, e2, e3) -> if (e1', e2, e3) --------------------------------------------(E-IfTrue) if(true, e2, e3) -> e2 --------------------------------------------(E-IfFalse) if(false, e2, e3) -> e3 e1 -> e1' --------------------------------------------(E-App1) e1 e2 -> e1' e2 e2 -> e2' --------------------------------------------(E-App2) (\lambda x:T.e) e2 -> (\lambda x:T.e) e2' --------------------------------------------(E-App3) (\lambda x:T.e) v -> [x|->v]e The static semantics makes use two environments: the typing environment G which maps any term variable x to its type T, and the kinding environment D from above. G -> . | x: T, G We present the following typing rules (the first try): -------------------------------------------------------(T-True) G; D |- true: Bool -------------------------------------------------------(T-False) G; D |- false: Bool G; D |- e1: Bool G; D |- e2: T G; D |- e3: T -------------------------------------------------------(T-If) G; D |- if(e1, e2, e3): T x:T \in G -------------------------------------------------------(T-Var) G; D |- x: T D |- T:: * G, x:T; D |- e: T' -------------------------------------------------------(T-Abs) G; D |- \lambda x:T.e: T->T' G; D |- e1: T1->T2 G; D |- e2: T1 -------------------------------------------------------(T-App) G; D |- e1 e2: T2 These rules are also similar to the rules from simply typed lambda calculus except for one rule T-Abs. Look at the T-Abs rule for abstraction, as the type annotation "T" is supplied explicitly by the programmers, so we must check that T is well-kinded, that's why we have the judgment: D |- T:: * we require the constructor T to be of arity 1 (a type). And I leave it as exercise to type check this expression: \lambda x: (\Lambda X::*. X) Bool. x However, our first try of typing rules is not satisfying: the rules are overly rigid; that is, the typing rules will reject some well-formed expressions. To see this, let's consider the following expression: (\lambda x:(\Lambda X::*.X)Bool.x) true which will be rejected by the current typing rules, for that the argument type of the \lambda abstraction is: (\Lambda X::*.X) Bool but the actural argument's type is: Bool which are not equivalent as required by the rule T-App. However, a close look at these two types reveals that they are beta equivalent, that is: (\Lambda X::*.X) Bool -> Bool So it's immediately clear that we should define a much fancier notion of "constructor equivalenece". As a first try, we may define a \beta-reduction relation on constructors much that what we did for the simply typed \lambda-calculus. The key rule looks like: (\Lambda X::K.T) T' -> [X|->T']T And to compare two constructors T1 and T2 for equivalence, we just normalize these two constructors by \beta-reducing them and then compare the results. Though simple to understand and implement, the above strategy will not work very smoothly. To see this, consider the following two constructors T1 and T2: T1 = \Lambda X::*.X T2 = \Lambda Y::*.Y it's easy to see that these two constructors should be considered equivalent up to \alpha-equivalence. However, this will force us to do more conversion on the constructors, say, to the De-Bruijn representation, which is annoying. Here is another example: T1 = (\Lambda X::*. (\Lambda Y::*.Y) X) T2 = \Lambda Y::*.Y to show that these two constructors are equivalent would require some rules like \eta-reduction. What we need here is an equational theory for constructor equivalence. For this purpose, we define a definitional equivalence relation "=~=" on two two constructors S and T, written as: S =~= T The rules for this relation include: -------------------------------------------------(Eq-Refl) T =~= T S =~= T -------------------------------------------------(Eq-Sym) T =~= S S1 =~= T1 S2 =~= T2 -------------------------------------------------(Eq-Arrow) S1->S2 =~= T1->T2 S =~= T -------------------------------------------------(Eq-Abs) \Lambda X::K.S =~= \Lambda X::K.T S1 =~= T1 S2 =~= T2 -------------------------------------------------(Eq-App) S1 S2 =~= T1 T2 -------------------------------------------------(Eq-Beta) (\Lambda X::K.T) S =~= [X|->S]T With these equivalence rules on constructors, we can now add a new typing rule to check constructor equivalence when necessary: G; D |- e: T T =~= S -------------------------------------------------(T-Eq) G; D |- e: S that is, if we can show an expression e is of type T and the type T is definitional equivalent to another type S, then we can conclude that the expression e is also of type S. Next, I will demonstrate the use of these typing rule by typing the following expression: (\lambda x:(\Lambda X::*.X) Bool. x) true and for brevity, I use the following abbreviations next: lam = (\lambda x:(\Lambda X::*.X) Bool. x)) ty = (\Lambda X::*.X)Bool->(\Lambda X::*.X)Bool the typing derivation is: --------------- X::* |- X::* ---------------------------- ------------ . |- \Lambda X::*.X::*=>* .|-Bool::* ------------------------------------------ ---------------------------------------------------- . |- (\Lambda X::*.X)Bool::* x:(\Lambda X::*.X)Bool;. |- x: (\Lambda X::*.X)Bool -------------------------------------------------------------------------------------------------- .;. |- lam: ty ty =~= Bool->Bool -------------------------------------------------------------------------------------------------- ---------------------- .;. |- lam: Bool->Bool .;. |- true: Bool -------------------------------------------------------------------------------------------------------------------------- .;. |- (\lambda x:(\Lambda X::*.X) Bool. x) true: Bool And I leave it an exercise to justify this equivalence relation: ty =~= Bool->Bool ------------------------------------ Metatheory of \lambda_\omega Suppose we are now writing a type checker for \lambda_\omage, it's not hard to see that the typing rules for this language is not syntax-directed, just like the rules for subtyping as we discussed, so we need to develop a metatheory for it, that is, we should develop a set of algorithmic typing rules. The key idea for the metatheory is that we should remove the T-Eq rule (as it's not syntax-directed) and incoporate the equivalence checking into various typing rules. For instance, the T-If rule and T-App rule should be modified to: G; D |- e1: T1 G; D |- e2: T2 G; D |- e3: T2 D |- T1 =~= Bool D |- T2 =~= T3 ----------------------------------------------------------(T-If) G; D |- if(e1, e2, e3): T2 G; D |- e1: T G; D |- e2: T' D |- T =~= T1->T2 D |- T1 ~= T' ----------------------------------------------------------(T-App) G; D |- e1 e2: T2 The definitional equivalence relation is also not syntax-directed, so we should also develop meta-theory for definitional equivalence. There are two key steps: (1) push down all constructors to arity 1 (i.e., kind *) by \eta-reduction; (2) normalize all arity-1 constructors to some normal form (the so called weak head normal form); and (3) compare, syntactically, the structural equivalence of the normal form. I leave the rules and algorithms to the programming assignment.