my blog lives here now
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

247 lines
11 KiB

6 years ago
2 years ago
  1. ---
  2. title: GADTs and Amulet
  3. date: March 27, 2018
  4. maths: true
  5. ---
  6. Dependent types are a very useful feature - the gold standard of
  7. enforcing invariants at compile time. However, they are still very much
  8. not practical, especially considering inference for unrestricted
  9. dependent types is equivalent to higher-order unification, which was
  10. proven to be undecidable.
  11. Fortunately, many of the benefits that dependent types bring aren't
  12. because of dependent products themselves, but instead because of
  13. associated features commonly present in those programming languages. One
  14. of these, which also happens to be especially easy to mimic, are
  15. _inductive families_, a generalisation of inductive data types: instead
  16. of defining a single type inductively, one defines an entire _family_ of
  17. related types.
  18. Many use cases for inductive families are actually instances of a rather
  19. less general concept, that of generalised algebraic data types, or
  20. GADTs: Contrary to the indexed data types of full dependently typed
  21. languages, these can and are implemented in several languages with
  22. extensive inference, such as Haskell, OCaml and, now, Amulet.
  23. Before I can talk about their implementation, I am legally obligated to
  24. present the example of _length indexed vectors_, linked structures whose
  25. size is known at compile time---instead of carrying around an integer
  26. representing the number of elements, it is represented in the type-level
  27. by a Peano[^1] natural number, as an _index_ to the vector type. By
  28. universally quantifying over the index, we can guarantee by
  29. parametricity[^2] that functions operating on these don't do inappropriate
  30. things to the sizes of vectors.
  31. ```ocaml
  32. type z ;;
  33. type s 'k ;;
  34. type vect 'n 'a =
  35. | Nil : vect z 'a
  36. | Cons : 'a * vect 'k 'a -> vect (s 'k) 'a
  37. ```
  38. Since the argument `'n` to `vect` (its length) varies with the constructor one
  39. chooses, we call it an _index_; On the other hand, `'a`, being uniform over all
  40. constructors, is called a _parameter_ (because the type is _parametric_ over
  41. the choice of `'a`). These definitions bake the measure of length into
  42. the type of vectors: an empty vector has length 0, and adding an element
  43. to the front of some other vector increases the length by 1.
  44. Matching on a vector reveals its index: in the `Nil` case, it's possible
  45. to (locally) conclude that it had length `z`. Meanwhile, the `Cons` case
  46. lets us infer that the length was the successor of some other natural
  47. number, `s 'k`, and that the tail itself has length `'k`.
  48. If one were to write a function to `map` a function over a `vect`or,
  49. they would be bound by the type system to write a correct implementation
  50. - well, either that or going out of their way to make a bogus one. It
  51. would be possible to enforce total correctness of a function such as
  52. this one, by adding linear types and making the vector parameter linear.
  53. ```ocaml
  54. let map (f : 'a -> 'b) (xs : vect 'n 'a) : vect 'n 'b =
  55. match xs with
  56. | Nil -> Nil
  57. | Cons (x, xs) -> Cons (f x, map f xs) ;;
  58. ```
  59. If we were to, say, duplicate every element in the list, an error would
  60. be reported. Unlike some others, this one is not very clear, and it
  61. definitely could be improved.
  62. ```
  63. Occurs check: The type variable jx
  64. occurs in the type s 'jx
  65. · Arising from use of the expression
  66. Cons (f x, Cons (f x, map f xs))
  67. 33 │ | Cons (x, xs) -> Cons (f x, Cons (f x, map f xs)) ;;
  68. │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  69. ```
  70. This highlights the essence of GADTs: pattern matching on them reveals
  71. equalities about types that the solver can later exploit. This is what
  72. allows the programmer to write functions that vary their return types
  73. based on their inputs - a very limited form of type-term dependency,
  74. which brings us ever closer to the Calculus of Constructions corner of
  75. Barendregt's lambda cube[^3].
  76. The addition of generalised algebraic data types has been in planning
  77. for over two years---it was in the original design document. In a
  78. mission that not even my collaborator noticed, all of the recently-added
  79. type system and IR features were directed towards enabling the GADT
  80. work: bidirectional type checking, rank-N polymorphism and coercions.
  81. All of these features had cover stories: higher-ranked polymorphism was
  82. motivated by monadic regions; bidirectional type checking was motivated
  83. by the aforementioned polymorphism; and coercions were motivated by
  84. newtype optimisation. But, in reality, it was a conspiracy to make GADTs
  85. possible: having support for these features simplified implementing our
  86. most recent form of fancy types, and while adding all of these in one go
  87. would be possible, doing it incrementally was a lot saner.
  88. While neither higher-ranked types nor GADTs technically demand a
  89. bidirectional type system, implementing them with such a specification
  90. is considerably easier, removing the need for workarounds such as boxy
  91. types and a distinction between rigid/wobbly type variables. Our
  92. algorithm for GADT inference rather resembles Richard Eisenberg's
  93. [Bake]{.textsc}[^4], in that it only uses local equalities in _checking_
  94. mode.
  95. Adding GADTs also lead directly to a rewrite of the solver, which now
  96. has to work with _implication constraints_, of the form `(Q₁, ..., Qₙ)
  97. => Q`, which should be read as "Assuming `Q₁` through `Qₙ`, conclude
  98. `Q`." Pattern matching on generalised constructors, in checking mode,
  99. captures every constraint generated by checking the right-hand side of a
  100. clause and captures that as an implication constraint, with all the
  101. constructor-bound equalities as assumptions. As an example, this lets us
  102. write a type-safe cast function:
  103. ```ocaml
  104. type eq 'a 'b = Refl : eq 'a 'a
  105. (* an inhabitant of eq 'a 'b is a proof that 'a and 'b are equal *)
  106. let subst (Refl : eq 'a 'b) (x : 'a) : 'b = x ;;
  107. ```
  108. Unfortunately, to keep inference decidable, many functions that depend
  109. on generalised pattern matching need explicit type annotations, to guide
  110. the type checker.
  111. When _checking_ the body of the function, namely the variable reference
  112. `x`, the solver is working under an assumption `'a ~ 'b` (i.e., `'a` and
  113. `'b` stand for the same type), which lets us unify the stated type of
  114. `x`, namely `'a`, with the return type of the function, `'b`.
  115. If we remove the local assumption, say, by not matching on
  116. `Refl`{.haskell}, the solver will not be allowed to unify the two type
  117. variables `'a` and `'b`, and an error message will be reported[^6]:
  118. ```
  119. examples/gadt/[11:43 ..11:43]: error
  120. Can not unify rigid type variable b with the rigid type variable a
  121. · Note: the variable b was rigidified because of a type ascription
  122. against the type forall 'a 'b. t 'a 'b -> 'a -> 'b
  123. and is represented by the constant bq
  124. · Note: the rigid type variable a, in turn,
  125. was rigidified because of a type ascription
  126. against the type forall 'a 'b. t 'a 'b -> 'a -> 'b
  127. · Arising from use of the expression
  128. x
  129. 11 │ let subst (_ : t 'a 'b) (x : 'a) : 'b = x ;;
  130. │ ~
  131. ```
  132. Our intermediate language was also extended, from a straightforward
  133. System F-like lambda calculus with type abstractions and applications,
  134. to a System F<sub>C</sub>-like system with _coercions_, _casts_, and
  135. _coercion abstraction_. Coercions are the evidence, produced by the
  136. solver, that an expression is usable as a given type---GADT patterns
  137. bind coercions like these, which are the "reification" of an implication
  138. constraint. This lets us make type-checking on the intermediate language
  139. fast and decidable[^5], as a useful sanity check.
  140. The two new judgements for GADT inference correspond directly to new
  141. cases in the `infer` and `check` functions, the latter of which I
  142. present here for completeness. The simplicity of this change serves as
  143. concrete evidence of the claim that bidirectional systems extend readily
  144. to new, complex features, producing maintainable and readable code.
  145. ```haskell
  146. check (Match t ps a) ty = do
  147. (t, tt) <- infer t
  148. ps <- for ps $ \(p, e) -> do
  149. (p', ms, cs) <- checkPattern p tt
  150. let tvs = unTvName (boundTvs p' ms)
  151. (p',) <$> implies (Arm p e) tt cs
  152. (local (typeVars %~ Set.union tvs)
  153. (extendMany ms (check e ty)))
  154. pure (Match t ps (a, ty))
  155. ```
  156. This corresponds to the checking judgement for matches, presented below.
  157. Note that in my (rather informal) theoretical presentation of Amulet
  158. typing judgements, we present implication constraints as a lexical scope
  159. of equalities conjoined with the scope of variables; Inference
  160. judgements (with double right arrows, $\Rightarrow$) correspond to uses of
  161. `infer`, pattern checking judgements ($\Leftarrow_\text{pat}$)
  162. correspond to `checkPattern`, which also doubles as $\mathtt{binds}$ and
  163. $\mathtt{cons}$, and the main checking judgement $\Leftarrow$ is the
  164. function `check`.
  165. $$
  166. \frac{\Gamma; \mathscr{Q} \vdash e \Rightarrow \tau
  167. \quad \Gamma \vdash p_i \Leftarrow_\text{pat} \tau
  168. \quad \Gamma, \mathtt{binds}(p_i); \mathscr{Q}, \mathtt{cons}(p_i)
  169. \vdash e_i \Leftarrow \sigma}
  170. {\Gamma; \mathscr{Q} \vdash \mathtt{match}\ e\ \mathtt{with}\ \{p_i \to
  171. e_i\} \Leftarrow \sigma}
  172. $$
  173. Our implementation of the type checker is a bit more complex, because it
  174. also does (some) elaboration and bookkeeping: tagging terms with types,
  175. blaming type errors correctly, etc.
  176. ---
  177. This new, complicated feature was a lot harder to implement than
  178. originally expected, but in the end it worked out. GADTs let us make the
  179. type system _stronger_, while maintaining the decidable inference that
  180. the non-fancy subset of the language enjoys.
  181. The example presented here was the most boring one possible, mostly
  182. because [two weeks ago] I wrote about their impact on the language's
  183. ability to make things safer.
  184. [^1]: Peano naturals are one particular formulation of the natural
  185. numbers, which postulates that zero (denoted `z` above) is a natural
  186. number, and any natural number's successor (denoted `s 'k` above) is
  187. itself natural.
  188. [^2]: This is one application of Philip Wadler's [Theorems for Free]
  189. technique: given a (polymorphic) type of some function, we can derive
  190. much of its behaviour.
  191. [^3]: Amulet is currently somewhere on the edge between λ2 - the second
  192. order lambda calculus, System F, and λP2, a system that allows
  193. quantification over types and terms using the dependent product form,
  194. which subsumes both the ∀ binder and the → arrow. Our lack of type
  195. functions currently leaves us very far from the CoC.
  196. [^4]: See [his thesis]. Our algorithm, of course, has the huge
  197. simplification of not having to deal with full dependent types.
  198. [^5]: Even if we don't do it yet---work is still ongoing to make the
  199. type checker and solver sane.
  200. [^6]: And quite a good one, if I do say so! The compiler
  201. syntax highlights and pretty-prints both terms and types relevant to the
  202. error, as you can see [here].
  203. [Theorems for Free]:
  204. [his thesis]:
  205. [two weeks ago]: /posts/2018-03-14.html
  206. [here]: