2.7 Functional dependencies
3.1.1 Simple example: Length-indexed vectors
We start by examining length-indexed vectors. This well-worn example is still useful, as it is easy to understand and still can show off many of the new features of Dependent Haskell.
3.1.1.1 Vec definition
Here is the definition of a length-indexed vector:
dataNat =Zero |Succ Nat -- first, some natural numbers
dataVec::Type→Nat →Typewhere Nil ::Vec a ’Zero
(:>) ::a →Vec a n→Vec a(’Succ n)
infixr5 :>
I will use ordinary numerals as elements ofNat in this text.5 The Vec type is parame- terized by both the type of the vector elements and the length of the vector. Thus
True:>Nil has type Vec Bool 1 and ’x’:>’y’:>’z’:>Nil has typeVec Char 3. While Vec is a fairly ordinary GADT, we already see one feature newly introduced by my work: the use of Type in place of ?. Using ? to classify ordinary types is troublesome because ? can also be a binary operator. For example, should F ?Int be a function F applied to ? and Int or the function ? applied toF and Int? In order to avoid getting caught on this detail, Dependent Haskell introduces Type to classify ordinary types. (Section 7.4 discusses a migration strategy from legacy Haskell code that uses ?.)
Another question that may come up right away is about my decision to use Nats in the index. Why not Integers? In Dependent Haskell,Integers are indeed available in types. However, since we lack simple definitions forInteger operations (for example, what is the body of Integer’s+ operation?), it is hard to reason about them in types. This point is addressed more fully in Section 7.5. For now, it is best to stick to the simpler Nat type.
3.1.1.2 append
Let’s first write an operation that appends two vectors. We already need to think carefully about types, because the types include information about the vectors’ lengths. In this case, if we combine a Vec a n and a Vec a m, we had surely better get a
Vec a(n+m). Because we are working over ourNat type, we must first define addition:
(+) ::Nat →Nat →Nat Zero +m=m
Succ n+m=Succ (n+m)
Now that we have worked out the hard bit in the type, appending the vectors themselves is easy:
5In contrast, numerals used in types in GHC are elements of a built-in typeNat that uses a more
append::Vec a n→Vec a m→Vec a(n’+m)
append Nil w =w
append (a:>v)w =a:>(append v w)
There is a curiosity in the type of append: the addition between n andm is performed by the operation ’+. Yet we have defined the addition operation+. What’s going on here?
Haskell maintains two separate namespaces: one for types and one for terms. Doing so allows declarations like data X = X, where the data constructor X has type X. With Dependent Haskell, however, terms may appear in types. (And types may, less frequently, appear in terms; see Section 3.1.3.2.) We thus need a mechanism for telling the compiler which namespace we want. In a construct that is syntactically a type (that is, appearing after a :: marker or in some other grammatical location that is “obviously” a type), the default namespace is the type namespace. If a user wishes to use a term-level definition, the term-level definition is prefixed with a ’. Thus, ’+ simply uses the term-level + in a type. Note that the ’ mark has no semantic content—it is not a promotion operator. It is simply a marker in the source code to denote that the following identifier lives in the term-level namespace.
The fact that Dependent Haskell allows us to use our old, trusty, term-level +in a type is one of the two chief qualities that makes it a dependently typed language.
3.1.1.3 replicate
Let’s now write a function that can create a vector of a given length with all elements equal. Before looking at the function over vectors, we’ll start by considering a version of this function over lists:
listReplicate::Nat →a →[a]
listReplicate Zero = [ ]
listReplicate (Succ n)x =x :listReplicate n x
With vectors, what will the return type be? It surely will mention the element type a, but it also has to mention the desired length of the list. This means that we must give a name to the Nat passed in. Here is how it is written in Dependent Haskell:
replicate::∀a.Π (n::Nat)→a→Vec a n replicate Zero =Nil
replicate (Succ n)x =x:>replicate n x
The first argument to replicate is bound by Π (n::Nat). Such an argument is available for pattern matching at runtime but is also available in the type. We see the value n
used in the resultVec a n. This is an example of a dependent pattern match, and how this function is well-typed is considered is some depth in Section 4.3.3.
The ability to have an argument available for runtime pattern matching and compile-time type checking is the other chief quality that makes Dependent Haskell dependently typed.
3.1.1.4 Invisibility in replicate
The first parameter to replicate above is actually redundant, as it can be inferred from the result type. We can thus write a version with this type:
replicateInvis:: Π (n::Nat).∀a.a→Vec a n
Note that the type begins withΠ (n::Nat). instead of Π (n::Nat)→. The use of the . there recalls the existing Haskell syntax of∀a., which denotes an invisible argument
a. Invisible arguments are omitted at function calls and definitions. On the other hand, the → in Π (n::Nat) → means that the argument is visible and must be provided at every function invocation and defining equation. This choice of syntax is due to Gundry [37]. Some readers may prefer the terms explicit and implicit to describe visibility; however, these terms are sometimes used in the literature (e.g., [64]) when talking about erasure properties. I will stick to visible andinvisible throughout this dissertation.
We can now use type inference to work out the value of n that should be used:
fourTrues::Vec Bool 4
fourTrues =replicateInvis True
How should we implement replicateInvis, however? We need to use an invisibility override. The implementation looks like this:
replicateInvis @Zero =Nil
replicateInvis @(Succ )x =x:>replicateInvis x
The @in those patterns means that we are writing an ordinarily invisible argument visibly. This is necessary in the body ofreplicateInvisas we need to pattern match on the choice of n. An invisibility override can also be used at call sites: replicateInvis @2’q’
produces the vector ’q’:>’q’:>Nil of type Vec Char 2. It is useful when we do not know the result type of a call to replicateInvis.6
3.1.1.5 Computing the length of a vector
Given a vector, we would like to be able to compute its length. At first, such an idea might seem trivial—the length is right there in the type! However, we must be careful here. While the length is indeed in the type, types are erased in Haskell. That length is thus not automatically available at runtime for computation. We have two choices for our implementation of length:
lengthRel :: Πn.∀a.Vec a n→Nat lengthRel @n =n
lengthIrrel::∀n a.Vec a n→Nat lengthIrrel Nil = 0
lengthIrrel ( :>v) = 1 +lengthIrrel v
The difference between these two functions is whether or not they quantify nrelevantly. A relevant parameter, bound by Π, is one available at runtime.7 In lengthRel, the type declares that the value of n, the length of the Vec a n is available at runtime. Accordingly,lengthRel can simply return this value. The one visible parameter, of type
Vec a n is needed only so that type inference can infer the value of n. This value must be somehow known at runtime in the calling context, possibly because it is statically known (as in lengthRel fourTrues) or because n is available relevantly in the calling function.
On the other hand, lengthIrrel does not need runtime access to n; the length is computed by walking down the vector and counting the elements. When lengthRel is available to be called, bothlengthRel andlengthIrrel should always return the same value. (In contrast,lengthIrrel is always available to be called.)
The choice of relevant vs. irrelevant parameter is denoted by the use of Π or ∀in the type: lengthRel says Π n whilelengthIrrel says ∀n. The programmer must choose between relevant and irrelevant quantification when writing or calling functions. (See Section 8.7 for a discussion of how this choice relates to decisions in other dependently typed languages.)
We see also that lengthRel takes n before a. Both are invisible, but the order is important because we wish to bind the first one in the body of lengthRel. If I had written lengthRel’s type beginning with ∀a.Πn., then the body would have to be
lengthRel @ @n =n.
3.1.1.6 Conclusion
These examples have warmed us up to examine more complex uses of dependent types in Haskell. We have seen the importance of discerning the relevance of a parameter, invisibility overrides, and dependent pattern matching.
7This is a slight simplification, as relevance still has meaning in types that are erased. See Section