** 8. Type System Design (as in ML), Part 2 ** ---------------------------------------------------------------------------- 8.1. Next we'll add recursive types. E.g. lists: intList ::= Nil | IntPair of int * intList (Note that recursive type is made up of union types and product types.) (We'll make this polymorphic in a little while.) We need a way to allow the IntList self-reference. One way is to add some kind of recursive type declaration, e.g.: e ::= ... | let rec type id = tau in e In this expression, the id being bound can be referenced within the tau to make a self-reference, e.g.: let rec type intList = [Nil: unit, IntPair: int * intList] in ... One weakness of this design is that recursive types cannot be expressed without giving them a name and a scope as part of a let declaration. Unlike all previous kinds of types, we can't just add a new case to the tau grammar rule. An alternative to a special kind of let is to introduce an *anonymous* recursive type *expression*, e.g.: tau ::= ... | rec id = tau (rec is often written as mu in the literature.) In this type expression, id is a local identifier, bound inside the tau, which refers to the resulting type. We can define lists of integers now without giving it an (external) name: rec intList = [Nil: unit, IntPair: int * intList] This is a type expression, and can be used wherever a type expression is allowed, e.g. as the argument type of a lambda, or as the type of an identifier bound in a let-val binding, or as the type bound to a name in a let-type binding. E.g., if we want a more compact name, then we can say, using the standard let-type construction: let type intList = (rec intList = [Nil: unit, IntPair: int * intList]) in ... The advantage of the rec type construct is that it fits in orthogonally with our existing language. It keeps name binding a separate, orthogonal, independent notion from recursion. It is, however, syntactically more verbose, but real languages can provide sugar in normal cases. ---------------------------------------------------------------------------- 8.2. What are the equality rules for two recursive types? Which of the following types are equal: rec intList = [Nil: unit, IntPair: int * intList] rec fooList = [Nil: unit, IntPair: int * fooList] let quxList = [Nil: unit, IntPair: int * fooList] in [Nil: unit, IntPair: int * quxList] rec barList = [Nil: unit, IntPair: int * (rec bazList = [Nil: unit, IntPair: int * barList])] To define equality over potentially recursive types, that doesn't take into account accidents of how the types were written down, we define type equality of two types as structural equality *after infinite unfolding of any recursive types*. If two types have the same infinite unfolding, independent of how they were notated, then they're (structurally) equal. Of course, the trick is how to do this comparison without actually building the infinite unfolding, but there exist polynomial-time, deterministic algorithms for doing this. ---------------------------------------------------------------------------- 8.3. What are the operations on a value of recursive type? Previously, we added introduction and elimination forms for new types, to help think about the operations on the type. Pursuing this design principle, we can introduce "fold" as the introduction form (take a value of type tau and return a value of type rec id=tau) and "unfold" as the elimination form (vice versa). e ::= ... | fold e as tau | unfold e E.g.: val l1 = fold [Nil=()] as intList val l2 = fold [IntPair=(3, l1)] as intList case (unfold l2) of [Nil=()] => ... | [IntPair=(x,l)] => ... Now we use these operations to convert between the recursive and non-recursive views of values. E.g., map over int lists: val map:((int->int) * intList) -> intList = lambda (f:int->int, l:intList). case (unfold l) of [Nil=()] => l | [IntPair=(x,l')] => fold [IntPair=(f x, map (f,l'))] as intList This code first unfolds its list argument (to expose the union), and then folds up the resulting mapped list when it's done. Without recursive types, we couldn't write functions that worked uniformly over objects of unbounded size. (Actually, this function definition doesn't work, because the recursive reference to map isn't defined in the body of the lambda, only in code after the top-level val declaration. We'll fix this shortly.) These operations only change our view of the underlying type, they needn't actually do any work at run-time. They're just there to help the typechecker do its work without any real smarts. This kind of recursive type is called an iso-recursive type, since the fold & unfold operations show how the recursive type is isomorphic (but different than) its unfolding. But all this folding and unfolding can be a pain. An alternative is just to make the recursive type equivalent to its unfolding, called an equi-recursive type, and so all the operations on the unfolded type can be performed on the folded type as well. E.g., the following would be legal: val l1 = [Nil=()] val l2 = [IntPair=(3, l1)] case l2 of [Nil=()] => ... | [IntPair=(x,l)] => ... val map:((int->int) * intList) -> intList = lambda (f:int->int, l:intList). case l of [Nil=()] => l | [IntPair=(x,l')] => [IntPair=(f x, map (f,l'))] Equi-recursive types are clearly what programmers want, but they're difficult to do in general, particularly to support type inference, so most formal systems use iso-recursive types, which are very clear. ML's use of recursive types (i.e., datatypes) are restricted enough that it's clear where the fold and unfold operations go; it's a kind of type inference problem that's solvable. ---------------------------------------------------------------------------- 8.4. The rec idea works just as well for values as it does for types. If we want to allow recursive functions in our language, we introduce rec value expressions: e ::= ... | rec id:tau = e E.g. (rec f:int->int = lambda x:int. if x <= 1 then 1 else x * f (x - 1)) 3 We can of course bind such a thing to a name, e.g. fact, but this is a separate, orthogonal construct. If the right-hand-side expression of the rec construct is a simple value, such as a lambda expression, then rec makes operational sense. But if it's a regular computation, it can be difficult to implement, or even to precisely define its formal semantics. E.g. rec x:int = x + 1 So real languages only allow recursive function values. (C.f. Scheme's letrec, which only applies to functions.) It's probably also safe to allow other purely constructed values on the right-hand-side, e.g. pairs, lists, unions. E.g., an infinite (circular) list of 1's: rec l:intList = [IntPair=(1, l)] ---------------------------------------------------------------------------- 8.5. We also would like mutually recursive types and values. We can treat mutually recursive values as a self-recursive tuple of values, using existing constructs. E.g. the mutually recursive function declaration in ML: fun f (x:tau_x) = ... f ... g ... h ... and g (y:tau_y) = ... f ... g ... h ... and h (z:tau_z) = ... f ... g ... h ... can be desugared into the core language constructs val (f, g, h) = rec t:(tau_f * tau_g * tau_h) = (lambda x:tau_x. ... (#1 t) ... (#2 t) ... (#3 t) ..., lambda y:tau_y. ... (#1 t) ... (#2 t) ... (#3 t) ..., lambda z:tau_z. ... (#1 t) ... (#2 t) ... (#3 t) ...) or, using the pattern-matching that we can do in rec expressions, val (f, g, h) = rec (f':tau_f, g':tau_g, h':tau_h) = (lambda x:tau_x. ... f' ... g' ... h' ..., lambda y:tau_y. ... f' ... g' ... h' ..., lambda z:tau_z. ... f' ... g' ... h' ...) Mutually recursive type expressions could be written as follows: rec t1 = tau1 and t2 = tau2 where both tau1 and tau can contain references to t1 and t2. With this form, though, we don't have a way to bind two type names to the two component types that are visible outside the rec declaration (let only binds one type name). We could merge the rec type construct with the type- or value-level let construct, as in real ML: let type t1 = tau1 and type t2 = tau2 in end Alternatively, and probably best for our core language, we can simply exploit structural equality of types, treating each type in the mutually recursive group as a separate self-recursive type that simply shares structure with other types. E.g., we can desugar the following collection of mutually recursive types: let type t1 = [Nil:unit, Pair:t2] and type t2 = int * t1 into two separate self-recursive type declarations: let type t1 = (rec t = [Nil:unit, Pair:int * t]) let type t2 = (rec t = int * [Nil:unit, Pair:t]) Now t1 and t2 have the exact same meaning as in the mutually recursive declarations, because the type of the Pair component of t1's union has the exact same structure (after infinitely unfolding the type recursion) as t2. But we don't need any new constructs in our core language to achieve this. ---------------------------------------------------------------------------- 8.6. Two types are considered equal if they're structurally equivalent (after replacing all let-bound identifiers by their definitions). But just because some value is represented as a pair of integers doesn't mean it's of the same logical type as every other pair of integers. What we want is to be able to "brand" a type with some sort of label that makes it unique, different from every other structurally equivalent type, branded or otherwise. (Modula-3 has branded types explicitly, while ML has branded types implicitly as part of its datatype declaration.) So we add: tau ::= ... | branded tau e ::= ... | brand e as tau | unbrand e p ::= ... | brand e as tau E.g.: type complex = branded (int * int) type point = branded (int * int) val p:point = brand (3,4) as point val c:complex= brand (3,4) as complex val manhattan_distance:point->int = lambda p:point. let (x:int, y:int) = unbrand p in x + y (* or: *) val manhattan_distance:point->int = lambda (brand (x,y) as point):point. x + y manhattan_distance p (* OK *) manhattan_distance c (* not type-correct *) Now an occurrence of "branded tau" for some tau is not structurally equal to anything else, even another occurrence of "branded tau". But we must allow it to be equal to itself, otherwise it's useless. But how to identify self from other? One way is to augment the branded to include an explicit tag: tau ::= ... | branded id tau E.g.: type complex = branded Complex (int * int) type point = branded Point (int * int) Now the let-type bindings are pure short-hand, and we can replace all references to complex with its definition, safely: val p:point = brand (3,4) as branded Point (int * int) val c:complex= brand (3,4) as branded Complex (int * int) val manhattan_distance:(branded Point (int * int))->int = lambda p:(branded Point (int * int)). let (x:int, y:int) = unbrand p in x + y (* or: *) val manhattan_distance:(branded Point (int * int))->int = lambda (brand (x,y) as (branded Point (int * int))) :(branded Point (int * int)). x + y manhattan_distance p (* OK *) manhattan_distance c (* not type-correct *) Actually, we just said that two branded types are structurally equal if their brands are the same and their element types are structurally equal. We can make the first form syntactic sugar, where the system invents a distinct brand for each static occurrence in the source program of the type. To use such a system-generated branded type, let bindings are virtually required, so that the type can be referenced more than once without generating distinct types. And, in fact, the explicitly tagged form of branding becomes sugar itself, since we can get the same effect just by using a record of one field: branded id tau ==> {id:tau} Some languages, ML included, need to be able to generate new id tags each time a branded type expression is evaluated (not just each static occurrence in the program). This form is more difficult to model in the framework I've set up so far, and doesn't devolve to syntactic sugar for records. Type with these dynamic brands are called "generative". ---------------------------------------------------------------------------- 8.7. ML datatypes combine several core typing concepts into a single source-language-level feature: let-type bindings, branded types, recursive types, and union types. Datatypes are the only way for an ML programmer to access branded types and union types. A single datatype declaration also generates a number of function and constant values, as well as a declaring a type. A datatype declaration of the form: datatype Id = Constructor1 of Type1 | ... | ConstructorN of TypeN translates into the following core language expression: type Id = rec Id = branded [Constructor1: Type1, ... ConstructorN: TypeN] val Constructor1: Type1 -> Id ... val ConstructorN: TypeN -> Id The "of Type" part of a constructor can be omitted, in which case the type and values are changed as follows: type Id = rec Id = branded Id [Constructor1: Type1, ... Constructori: unit, ... ConstructorN: TypeN] val Constructor1: Type1 -> Id = lambda arg:Type1. fold (brand Id [Constructor1=arg]) as Id ... val Constructori: Id = fold (brand Id [Constructori=()]) as Id ... val ConstructorN: TypeN -> Id = lambda arg:TypeN. fold (brand Id [ConstructorN=arg]) as Id The i'th constructor is a value of type Id, not a function to build one. Datatype declarations also introduce pattern-matching for testing and projecting values out of the union. We can translate these into pattern-matching in our core language. E.g., the ML case expression ... x:Id ... case x of Constructor1 p1 => ... | ... | Constructori => ... | ... | ConstructorN pN => ... translates into the core case expression ... x:Id ... case (unbrand (unfold x)) of [Constructor1=p1] => ... | ... | [Constructori=()] => ... | ... | [ConstructorN=pN] => ...