Mercurial > urweb
comparison doc/tlc.ur @ 1505:8c851e5508a7
Tutorial: up to First-Class Polymorphism
author | Adam Chlipala <adam@chlipala.net> |
---|---|
date | Sun, 17 Jul 2011 11:00:04 -0400 |
parents | 71fdaef3b5dd |
children | 44fda91f5fa0 |
comparison
equal
deleted
inserted
replaced
1504:71fdaef3b5dd | 1505:8c851e5508a7 |
---|---|
1 (* Chapter 2: Type-Level Computation *) | 1 (* Chapter 2: Type-Level Computation *) |
2 | |
3 (* begin hide *) | |
4 val show_string = mkShow (fn s => "\"" ^ s ^ "\"") | |
5 (* end *) | |
2 | 6 |
3 (* This tutorial by <a href="http://adam.chlipala.net/">Adam Chlipala</a> is licensed under a <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/">Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License</a>. *) | 7 (* This tutorial by <a href="http://adam.chlipala.net/">Adam Chlipala</a> is licensed under a <a href="http://creativecommons.org/licenses/by-nc-nd/3.0/">Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License</a>. *) |
4 | 8 |
5 (* The last chapter reviewed some Ur features imported from ML and Haskell. This chapter explores uncharted territory, introducing the features that make Ur unique. *) | 9 (* The last chapter reviewed some Ur features imported from ML and Haskell. This chapter explores uncharted territory, introducing the features that make Ur unique. *) |
6 | 10 |
31 The type annotation on <tt>r</tt> uses the record concatenation operator <tt>++</tt>. Ur enforces that any concatenation happens between records that share no field names. Otherwise, we'd need to resolve field name ambiguity in some predictable way, which would force us to treat <tt>++</tt> as non-commutative, if we are to maintain the nice modularity properties of polymorphism. However, treating <tt>++</tt> as commutative, and treating records as equal up to field permutation in general, are very convenient for type inference and general programmer experience. Thus, we enforce disjointness to keep things simple.<br> | 35 The type annotation on <tt>r</tt> uses the record concatenation operator <tt>++</tt>. Ur enforces that any concatenation happens between records that share no field names. Otherwise, we'd need to resolve field name ambiguity in some predictable way, which would force us to treat <tt>++</tt> as non-commutative, if we are to maintain the nice modularity properties of polymorphism. However, treating <tt>++</tt> as commutative, and treating records as equal up to field permutation in general, are very convenient for type inference and general programmer experience. Thus, we enforce disjointness to keep things simple.<br> |
32 <br> | 36 <br> |
33 For a polymorphic function like <tt>project</tt>, the compiler doesn't know which fields a type-level record variable like <tt>ts</tt> contains. To enable self-contained type-checking, we need to declare some constraints about field disjointness. That's exactly the meaning of syntax like <tt>[r1 ~ r2]</tt>, which asserts disjointness of two type-level records. The disjointness clause for <tt>project</tt> asserts that the name <tt>nm</tt> is not used by <tt>ts</tt>. The syntax <tt>[nm]</tt> is shorthand for <tt>[nm = ()]</tt>, which defines a singleton record of kind <tt>{Unit}</tt>, where <tt>Unit</tt> is the degenerate kind inhabited only by the constructor <tt>()</tt>.<br> | 37 For a polymorphic function like <tt>project</tt>, the compiler doesn't know which fields a type-level record variable like <tt>ts</tt> contains. To enable self-contained type-checking, we need to declare some constraints about field disjointness. That's exactly the meaning of syntax like <tt>[r1 ~ r2]</tt>, which asserts disjointness of two type-level records. The disjointness clause for <tt>project</tt> asserts that the name <tt>nm</tt> is not used by <tt>ts</tt>. The syntax <tt>[nm]</tt> is shorthand for <tt>[nm = ()]</tt>, which defines a singleton record of kind <tt>{Unit}</tt>, where <tt>Unit</tt> is the degenerate kind inhabited only by the constructor <tt>()</tt>.<br> |
34 <br> | 38 <br> |
35 The last piece of this puzzle is the easiest. In the example call to <tt>project</tt>, we see that the only parameters passed are the one explicit constructor parameter <tt>nm</tt> and the value-level parameter <tt>r</tt>. The rest are inferred, and the disjointness proof obligation is discharged automatically. The syntax <tt>#A</tt> denotes the constructor standing for first-class field name <tt>A</tt>, and we pass all constructor parameters to value-level functions within square brackets (which bear no formal relation to the syntax for type-level record literals <tt>[A = c, ..., A = c]</tt>). *) | 39 The last piece of this puzzle is the easiest. In the example call to <tt>project</tt>, we see that the only parameters passed are the one explicit constructor parameter <tt>nm</tt> and the value-level parameter <tt>r</tt>. The rest are inferred, and the disjointness proof obligation is discharged automatically. The syntax <tt>#A</tt> denotes the constructor standing for first-class field name <tt>A</tt>, and we pass all constructor parameters to value-level functions within square brackets (which bear no formal relation to the syntax for type-level record literals <tt>[A = c, ..., A = c]</tt>). *) |
40 | |
41 | |
42 (* * Basic Type-Level Programming *) | |
43 | |
44 (* To help us express more interesting operations over records, we will need to do some type-level programming. Ur makes that fairly congenial, since Ur's constructor level includes an embedded copy of the simply-typed lambda calculus. Here are a few examples. *) | |
45 | |
46 con id = fn t :: Type => t | |
47 | |
48 val x : id int = 0 | |
49 val x : id float = 1.2 | |
50 | |
51 con pair = fn t :: Type => t * t | |
52 | |
53 val x : pair int = (0, 1) | |
54 val x : pair float = (1.2, 2.3) | |
55 | |
56 con compose = fn (f :: Type -> Type) (g :: Type -> Type) (t :: Type) => f (g t) | |
57 | |
58 val x : compose pair pair int = ((0, 1), (2, 3)) | |
59 | |
60 con fst = fn t :: (Type * Type) => t.1 | |
61 con snd = fn t :: (Type * Type) => t.2 | |
62 | |
63 con p = (int, float) | |
64 val x : fst p = 0 | |
65 val x : snd p = 1.2 | |
66 | |
67 con mp = fn (f :: Type -> Type) (t1 :: Type, t2 :: Type) => (f t1, f t2) | |
68 | |
69 val x : fst (mp pair p) = (1, 2) | |
70 | |
71 (* Actually, Ur's constructor level goes further than merely including a copy of the simply-typed lambda calculus with tuples. We also effectively import classic <b>let-polymorphism</b>, via <b>kind polymorphism</b>, which we can use to make some of the definitions above more generic. *) | |
72 | |
73 con fst = K1 ==> K2 ==> fn t :: (K1 * K2) => t.1 | |
74 con snd = K1 ==> K2 ==> fn t :: (K1 * K2) => t.2 | |
75 | |
76 con twoFuncs :: ((Type -> Type) * (Type -> Type)) = (id, compose pair pair) | |
77 | |
78 val x : fst twoFuncs int = 0 | |
79 val x : snd twoFuncs int = ((1, 2), (3, 4)) | |
80 | |
81 | |
82 (* * Type-Level Map *) | |
83 | |
84 (* The examples from the same section may seem cute but not especially useful. In this section, we meet <tt>map</tt>, the real workhorse of Ur's type-level computation. We'll use it to type some useful operations over value-level records. A few more pieces will be necessary before getting there, so we'll start just by showing how interesting type-level operations on records may be built from <tt>map</tt>. *) | |
85 | |
86 con r = [A = int, B = float, C = string] | |
87 | |
88 con optionify = map option | |
89 val x : $(optionify r) = {A = Some 1, B = None, C = Some "hi"} | |
90 | |
91 con pairify = map pair | |
92 val x : $(pairify r) = {A = (1, 2), B = (3.0, 4.0), C = ("5", "6")} | |
93 | |
94 con stringify = map (fn _ => string) | |
95 val x : $(stringify r) = {A = "1", B = "2", C = "3"} | |
96 | |
97 (* We'll also give our first hint at the cleverness within Ur's type inference engine. The following definition type-checks, despite the fact that doing so requires applying several algebraic identities about <tt>map</tt> and <tt>++</tt>. This is the first point where we see a clear advantage of Ur over the type-level computation facilities that have become popular in GHC Haskell. *) | |
98 | |
99 fun concat [f :: Type -> Type] [r1 :: {Type}] [r2 :: {Type}] [r1 ~ r2] | |
100 (r1 : $(map f r1)) (r2 : $(map f r2)) : $(map f (r1 ++ r2)) = r1 ++ r2 | |
101 | |
102 | |
103 (* * First-Class Polymorphism *) | |
104 | |
105 (* The idea of <b>first-class polymorphism</b> or <b>impredicative polymorphism</b> has also become popular in GHC Haskell. This feature, which has a long history in type theory, is also central to Ur's metaprogramming facilities. First-class polymorphism goes beyond Hindley-Milner's let-polymorphism to allow arguments to functions to themselves be polymorphic. Among other things, this enables the classic example of Church encodings, as for the natural numbers in this example. *) | |
106 | |
107 type nat = t :: Type -> t -> (t -> t) -> t | |
108 val zero : nat = fn [t :: Type] (z : t) (s : t -> t) => z | |
109 fun succ (n : nat) : nat = fn [t :: Type] (z : t) (s : t -> t) => s (n [t] z s) | |
110 | |
111 val one = succ zero | |
112 val two = succ one | |
113 val three = succ two | |
114 | |
115 (* begin eval *) | |
116 three [int] 0 (plus 1) | |
117 (* end *) | |
118 | |
119 (* begin eval *) | |
120 three [string] "" (strcat "!") | |
121 (* end *) |