1.2 Dependent types
1.2.4 Common problems with dependent types
Thanks to the introduction of dependent types, we can now define more precise types, and we have the ability to write logical formulae. However,
these new possibilities also bring new problems. The first of them is that dependent types induce logical links between values and their types, and therefore obtaining an information about the shape of a term might give an information about some other terms. If the system doesn’t support these logical links, we will run into problems when defining functions. For example, when a function manipulates a natural number n and a
vectorvof typeVect n T, in the case wherenis zero, we know that v
has typeVect 0 T. However, in many systems, including the Coq proof assistant –outside of the proof mode–,vwill still be seen as an element of typeVect n T. Said differently, Coq doesn’t natively propagate the informations that we get from the shape of a value to the other types involved in the function. In Coq, one possiblity is to switch to the proof- mode and to do the case analysis with the tactic "destruct", or to still do it in the language-mode, but with the "match ... as ... return ..." construct which makes the definition slightly more complicated. Idris doesn’t have this complication, and if the shape of a term gives us informations about some other values, the system will know and use these informations. For example, the following (incomplete) code will typecheck :
f : (x:Nat) → (v: Vect x Bool) → Vect 0 Bool
f Z v = v
And that typechecks because Idris knows that v has effectively type
Vect0 Bool in the case wherex is zero.
This good support for dependent types that Idris has does not only propagate the information that we get by inspecting some values into the types of other values, but it also uses these informations to en- force the shape of these other values. For example, if a vector of type
Vect x Bool happens to be a Nil then x is forced to be Z, and the system knows it.
Every time that a term t of a dependent type (often a predicate) contains some information about some components of t, the system will collect this information and they will be facts that can then be used during the type-checking. For example, one way to define an Evenpredicate is :
data Even : Nat → Type where
We can use an element ofEven nin order to know that nhas the shape
x+xfor some x. This is extremely powerful because, as we notice here, the information about the shape isn’t forced to be a constructor : it can be a "non-primitive shape", such as x+x. Let’s use this information in the
definition of a function that divides by two any even natural number :
divideByTwo : {n:Nat} → (Even n) → Nat
divideByTwo {n=k+k} IsEven = k
We see that this definition of Even is particularly useful when the point of view being taken is that an even number can be divided by two7. Such a definition is possible because we have this powerful dependent pattern-matching which reveals informations not only about the shape of the expression which is being matched (here, the value of type Even n),
but also about other expressions (nin this case). We will see in chapter 5 that this powerful dependent matching also enables matching on some intermediate computations (still on the left hand side of definitions), and that it can deduce even more informations about the other expressions, with what will be calledviews.
In Coq, because there isn’t such a powerful dependent pattern match- ing built to its core type-theory, some functions are very difficult to define, and often, the last hope will be to define them in the proof-mode, as one would prove a theorem. However, the definition of a function done in proof-mode is very opaque, hard to understand and hard to maintain.
The presence of this powerful dependent pattern matching in Idris makes the definitions of many functions easier, especially the ones that manipulate terms of dependent types which contain links between several values, because the system will take in account the fact that knowing the shape of a term can affect the forms of other terms. This feature brings more structure to the programming activity, as the links between various data are becoming explicit.
However, there is still an important problem that will be encountered frequently when defining a function that returns a value of a dependent type. Often, the type of the function makes the claim that the output 7Another definition, more conventional but which does not help for defining the
has some specific indices (i.e. the output has type T i1 i2 ...in), but the
definition creates a term that has some different indices (i.e. a term of type T i01 i02...i0n), and the typechecker will simply reject this definition, as it produces something that does not have the expected type. If the definition makes sense, of course, the corresponding indices must be equal. By "equal", we mean something very specific called propositional equality, that will be detailed in chapter 2, but intuitively this equality means "provably equal", and implies "replaceable". This is in fact the very common and intuitive equality used in mathematics, where the left and right hand side of an =symbol can be written down differently, but must describe objects that are replaceable by each other. A propositional equality is therefore something that has to be proven, and in the case of such a function definition, the user of the system will have to prove that each index ik is equal to its corresponding i0k produced in the definition,
in order to make the definition accepted by the system. Said differently, when the programmer is defining a function with a slightly different type than expected, that’s his duty to prove the convertibility between the two types. That means that as soon as we start to introduce dependent types, we have proof obligations coming along the way. We will now present a concrete example of how these proof obligations arise in practice.