
Imperial College London Department of Computing Higher-order Type-level Programming in Haskell Supervisor Prof. Susan Eisenbach Csongor Kiss Second Marker Dr. Anthony Field June 18, 2018 1 Abstract Haskell, as implemented by the Glasgow Haskell Compiler (GHC), provides rich facilities for performing computation in the language of types, allowing programmers to ensure sophisticated static properties about their programs. The type language, however, has several limitations, restricting the range of expressible programs. Namely, the type language is first-order: no higher-order type functions are possible. This work aims to lift this restriction, enabling higher-order type-level programming. We motivate the problem by demonstrating the power of the type language, and explain the limitation. Then we describe a solution, and present an implementation in GHC. We discuss the interaction with the current type system, and present novel ways of writing type-level programs. 2 Acknowledgements I would like to thank my supervisor, Susan Eisenbach, and Tony Field for the motivating discussions and for keeping me on track. A particular thanks goes to Richard Eisenberg, who offered help and helpful insights about the topic. 3 Contents 1 Introduction5 1.1 Towards type safety...............................6 1.2 Type-level properties...............................6 1.3 Type families...................................8 1.4 Limitations....................................8 1.5 Contributions................................... 10 2 Background 11 2.1 Types and kinds................................. 11 2.2 Rich kinds..................................... 13 2.3 Equality...................................... 14 2.4 Type functions are first-order.......................... 17 2.5 Singletons..................................... 19 3 Unsaturated type families 21 3.1 A new arrow................................... 21 3.2 Type classes for type families.......................... 23 3.3 Code reuse.................................... 26 3.3.1 Subtyping................................. 26 3.3.2 Matchability polymorphism....................... 27 3.4 Type system................................... 28 3.4.1 Expressions................................ 28 3.4.2 Coercions................................. 29 3.4.3 Matchabilities............................... 30 3.4.4 Types and kinds............................. 30 3.4.5 Axioms.................................. 30 3.5 Type classes.................................... 30 3.5.1 Equality.................................. 32 Contents 4 Roles................................... 35 3.5.2 Instance resolution............................ 37 4 Implementation 41 4.1 Constraint-based type inference......................... 41 4.1.1 Constraints................................ 41 4.1.2 The solver pipeline............................ 42 Canonicalisation............................. 43 Flattening................................. 45 Canonicalising equalities......................... 45 Top-level interaction........................... 45 4.1.3 Unsaturated type families........................ 45 5 Unsaturated type families in practice 47 5.1 Generic programming.............................. 47 5.1.1 Algebraic structure............................ 48 5.1.2 Type-level traversals over the generic structure............ 50 5.2 Type-level Generic programming........................ 51 5.3 Stateful programming.............................. 54 6 Discussion 57 6.1 Dependent Haskell................................ 57 6.2 Future work.................................... 58 5 Chapter 1 Introduction Types help us write programs by making sure that programming errors are detected long before we attempt to execute them. The more sophisticated the type system, the more bad programs it rejects, and the more good programs it allows. Haskell is a purely functional programming language, which means that functions are referentially transparent: they are not allowed to mutate values or perform side-effects. While referential transparency already eliminates a large class of erroneous programs (such as those resulting from careless mutation of global variables), there are still many ways in which programs can fail. For example, consider the head function, which returns the first element of a list: -- pre: non-empty list head :: [a ] ! a head (x : xs) = x When all is well, head readily extracts the item in the first position1: > ghci> head [1 :: 10] > 1 However, if we try to interrogate the empty list, we are faced with a runtime error: > ghci> head [] > ? runtime error : non − exhaustive patterns in function head ? This is rather unfortunate, as the promise of static types was that they help us catch errors like these before we ever get to run our programs. However, not all is lost. We can write a safer version of head, which at least handles empty lists, and is explicit about when no value could be returned. 1Lines beginning with ghci> describe queries typed into the Glasgow Haskell Compiler's interactive REPL, and the following line is the corresponding result Chapter 1. Introduction 6 1.1 Towards type safety saferHead :: [a ] ! Maybe a saferHead (x : xs) = Just x saferHead [ ] = Nothing saferHead returns a value of type Maybe a. Users of the function must then pattern match on the result to discover whether the extraction succeeded (Just x) or not (Nothing). > ghci> saferHead [] > Nothing While we have at least managed to avoid runtime exceptions, this solution is still unsatis- factory. Even if we know that the input list is non-empty, we still have to pattern match on the result every time we wish to use saferHead. Ideally the compiler would just tell us that we tried to call head on the empty list, and reject the program. In vanilla Haskell98 [Peyton Jones, 2003], saferHead is the best we can do. Over the long course of Haskell's history, however, the language has been extended with a plethora of type system features that allow programmers to explain their informal knowledge (such as that the input list has to be non-empty) to the typechecker by means of a more expressive vocabulary in the type language. 1.2 Type-level properties Let us revisit the head function by using the full power of Haskell's type system, as imple- mented in the Glasgow Haskell Compiler (GHC)2. The requirement we wish to enforce is that the input list is non-empty, or in other words, that its length is at least one. For the built-in list type, the length of a list is not apparent in its type. For example, [ ] :: [Int ] and [1; 2] :: [Int ] both have the same type. In order to reason about the lengths, we need to reflect a notion of numeric values in the type system. To illustrate this consider the following inductive definition of natural numbers: data Nat = Zero j Succ Nat 2As of writing, the most recent released version is 8.4.3 Chapter 1. Introduction 7 At the value level, this introduces the data constructors Zero and Succ. The DataKinds language extension [Yorgey et al., 2012] allows the Zero and Succ data constructors also to be used at the type level as type constructors. We can then create a version of list, which we call Vector, that is indexed by the number of elements it contains. data Vector :: Nat ! ? ! ? where VNil :: Vector 'Zero a VCons :: a ! Vector n a ! Vector ('Succ n) a Here, Vector is a generalised algebraic data type (GADT) [Cheney and Hinze, 2003], which means that its constructors refine the type index. The empty constructor VNil simply represents a list whose length is 'Zero3. VCons takes a value of type a and another vector of length n, and constructs a vector of length one bigger than n. Now we can specify the the safeHead function as safeHead :: Vector ('Succ n) a ! a safeHead (VCons x xs) = x That is, it takes a vector whose length is one bigger than some natural number (i.e at least one). Since the only way to produce an index headed by 'Succ is by using VCons, calling safeHead with the empty list is a type error: > ghci> safeHead (VCons 1 (VCons 2 VNil)) > 1 > ghci> safeHead VNil > ? type error : Couldn0t match type 'Zero with ' Succ n0 ∗ Now it's impossible to try to extract the head of the empty list, as the compiler will stop us from doing so. 1.3 Type families GADTs are a powerful tool, but working with them can be awkward. For example, it's not obvious how to carry out certain operations on them that modify their lengths, such as appending two vectors together, as in order for this to be expressible, we need a way of performing computation (addition in this case) at the type level. With the TypeFamilies 3The ' (tick) symbol is used as a syntactic aid to signify that Zero is used in a type context here. Chapter 1. Introduction 8 extension [Chakravarty et al., 2005], we can express type functions (called type families) in a natural way. type family Add (a :: Nat)(b :: Nat) :: Nat where Add 'Zero b = b Add ('Succ a) b = 'Succ (Add a b) The Add type family carries out the addition of two Nats by doing recursion on the first. This allows us to express the type of append, that is: it takes two vectors and returns a new vector whose length is the sum of the two inputs. append :: Vector n a ! Vector m a ! Vector (Add n m) a append VNil v = v append (VCons a as) v = VCons a (append as v) 1.4 Limitations Type families, or type functions, enable a natural and convenient way of expressing type- level computations. However, the current implementation of type families is not ideal, in that there are some computations that can be expressed at the value level that cannot
Details
-
File Typepdf
-
Upload Time-
-
Content LanguagesEnglish
-
Upload UserAnonymous/Not logged-in
-
File Pages58 Page
-
File Size-