** 10. Polymorphic Types ** ---------------------------------------------------------------------------- 10.1. ML supports (parametrically) polymorphic functions, values, and types, which operate uniformly over values of different types. (To date, we've only studied monomorphic functions & values, which only have a single type.) E.g.: val (op ::): 'a * 'a list -> 'a list val hd: 'a list -> 'a val tl: 'a list -> 'a list val nil: 'a list val map: ('a -> 'b) -> 'a list -> 'b list val foldl: ('a * 'b -> 'a) -> 'b -> 'b list -> 'a val (op o): ('b -> 'c) -> ('a -> 'b) -> 'a -> 'c datatype 'a tree = Leaf of 'a | Node of 'a tree * 'a tree (* val Leaf: 'a -> 'a tree *) (* val Node: 'a tree * 'a tree -> 'a tree *) type ('a, 'b) alist = ('a * 'b) list The 'a et al. are *type variables*. To use a polymorphic value or type, one first *instantiates* the polymorphic type to get a particular monomorphic type, and then one proceeds as before. One must instantiate a given type variable to the same type in all its occurrences in the polymorphic type; this expresses constraints on the legal instantiating type. Some example explicit instantiations: type int_list = int list val int_nil = nil:int_list val int_cons = (op ::):int * int_list -> int_list val l = int_cons (3, int_cons (4, int_nil)) val m = map:(int->string) -> int_list -> string list val l2 = m Int.toString l type int_list_list = int_list list; val int_list_nil = nil:int_list_list ... Usually, local typing constraints is sufficient to determine the instantiation without user intervention, e.g.: val l2 = map Int.toString l In this expression, map can only be instantiated to (int->string) -> int_list -> string list in order to make the call work, and this can be told solely from the types of the arguments (easy for a typechecker). In some examples, only the resulting context can resolve which instantation to do, e.g.: let val copy = map (fn x => x) in ... end Here we don't know what instance of the list type to take & return, without seeing how the copy function is used within the let body. This is a job for full ML type inference, discussed later. ------------------------------------------------------------------------ 10.2. How should we make these ideas explicit in CoreML? The basic idea (as usual) is to introduce a new kind of type, polymorphic types, and introduction and elimination forms for this type. The simplest approach is the following. First, we introduce a polymorphic type: tau ::= ... | forall id. tau | id The type "forall id. tau" is a polymorphic type, with type variable id. id can be mentioned in tau, and refers to whatever type the polymorphic type will be instantiated with. Here's a polymorphic list type: type list = (* ML's "'a list" *) forall a. rec t = branded List [Nil:unit, Pair:a * t] We also have type expressions to instantiate a polymorphic type. This is a kind of elimination form at the type level (the forall form is the introduction form for polymorphic types). To model instantiation, we'll think of the polymorphic type as a function over types: given an argument type (the instantiating type), produce a result type (the instantiated type). This suggests that the elimination form for polymorphic types i.e. the instantiation type expression is just application (we introduce the brackets to match the syntax of the expression form introduced below): tau ::= ... | tau[tau] E.g., here're some instantiations of some polymorphic types: type int_list = list[int] (* ML's "int list" *) type int_list_list = list[list[int]] (* ML's "int list list" *) type cons_type = (* ML's "'a * 'a list -> 'a list" *) forall a. a * list[a] -> list[a] type int_cons_type = (* ML's "int * int list -> int list" *) cons_type[int] To have multiple type variables in a polymorphic type, we just nest forall's, e.g.: type alist = (* ML's "type ('a,'b) alist = ('a * 'b) list" *) forall a. forall b. list[a * b] This is a kind of curried function, at the type level. To instantiate, we do the same things as we would to invoke a curried function: type int_string_alist = alist[int][string] --------------------------------------------------------------------------- 10.3. OK, now for the expression forms. We want to be able to create values of polymorphic type (e.g. nil, map), and to instantiate those values to get monomorphically typed values that we can then manipulate using the previous constructs in our language. We'll follow the lead of the type language and think of polymorphic values as functions from types to values, leading to the following introduction and elimination rules: e ::= ... | Lambda id. e | e[tau] Here we'll use a capital lambda for functions taking types, in place of the lower-case lambda for functions taking values. The difference between Lambda and forall is that Lambda forms return values when applied, and hence are value expression forms, while forall forms return types when applied, and hence are type expression forms. (We use the brackets in the elimination form to apply a function to a type to distinguish it syntactically from applying a function to a value.) The following are formal typing rules for polymorphic value introduction & elimination rules: Gamma |- e : tau -------------------------- Gamma |- (Lambda id. e) : forall id. tau Gamma |- e : forall id. tau' -------------------------- Gamma |- (e[tau]) : tau'[id := tau] The first rule introduces a forall type for a Lambda expression, and the second rule eliminates a forall type by substituting its bound type variable with the given instantiating argument type. Some examples: val nil:list = Lambda a. fold brand List [Nil=()] as list[a] val int_nil:int_list = nil[int] val cons:cons_type = Lambda a. lambda (x:a, xs:list[a]). fold brand List [Pair=(x,xs)] as list[a] val l1:int_list = cons[int] 3 int_nil val map: forall a. forall b. (a->b) -> list[a] -> list[b] = Lambda a. Lambda b. rec map:(a->b) -> list[a] -> list[b] = lambda (f:a->b, l:list[a]). case unbrand unfold l of [Nil=()] => nil[list[b]] | [Pair=(x:a, xs:list[a])] => cons[list[b]] (f x) (map f xs) --------------------------------------------------------------------------- 10.4. In all these examples, the forall's have been outermost in the types; there were no polymorphic types as function arguments or list element types. This is a requirement of the ML type system, due to its type inference algorithm. But this prevents us from doing some interesting things. Here are some simple examples of what isn't allowed by ML's type system: (* make a list of n x's *) val mk_list:int -> (forall a. a -> list[a]) = lambda n:int. Lambda a. lambda x:a. if n = 0 then nil[a] else cons[a] x (mk_list[a] (n - 1) x) val mk_singleton:(forall a. a -> list[a]) = mk_list 1 val mk_many:(forall a. a -> list[a]) = mk_list 100 (* a function that takes a polymorphic function as an argument, and applies it to some different types internally *) val crazy: (forall a. a -> list[a]) -> (list[int] * list[bool]) = Lambda a. lambda (f:forall a. a -> list[a]). (f[int] 3, f[bool] true) crazy mk_singleton crazy mk_many These examples are somewhat contrived, but there are real examples that would benefit from being about pass in and return polymorphic values to functions, and to instantiate them differently in different contexts. --------------------------------------------------------------------------- 10.5. The typing rules given above don't quite work out. Previously, punted on type expressions containing identifiers, disallowing let-type forms for the language for which we gave formal typing rules. Here we've got type identifiers again. If we want to use structural equality of the tau's to determine when two types are equal, then we have to do processing of type expressions to (a) eliminate uses of type identifiers bound by let-type, replacing the identifiers with the types to which they were bound, and (b) cope with type identifiers introduced by Lambda forms, ensuring that identifiers bound to possibly distinct types are given distinct "dummy" type bindings, and identifiers known to be bound to the same type are given the same dummy type bindings. It's not too hard to extend our formalism to handle this, e.g. by introducing a new environment parallel to Gamma that holds a mapping of type identifiers to the types to which they're bound, introducing a fresh dummy type whenever binding a type identifier by Lambda, and "evaluating" any type expressions in the expression being typechecked to produce a canonical form where all type identifiers have been replaced with their bound types before using them in the rest of the typechecking process. But I won't work through the exercise in these notes.