• No results found

1.2 Dependent types

1.2.1 What dependent types are

Dependent types informally

The biggest novelty in functional programming compared to imperative or object-oriented programming is the fact that functions are first-class values5. In the same way, in dependently-typed programming languages, the novelty is that types –that were previously only a compile-time information– are now first-class values. They are something that can be computed, which means that they can be taken in parameter and returned by a function, just as any other value. It is for example possible to write this function :

NatOrList : (b:Bool) → Type

NatOrList False = Nat NatOrList True = List Nat

This function returns a type, so for a given boolean b, the expression

(NatOrList b) is a type, and this type depends on the valueb : ifb

isFalsethen this type isNat, ifbisTruethen this type isListofNat.

With this, we can write a function that produces a value that can have two different possible types, depending on the actual value of the input.

ZeroOrNil : (b:Bool) → NatOrList b

ZeroOrNil False = Z ZeroOrNil True = []

Thetypeof the output of this function depends on thevalueof the input, and therefore this function has adependent type.

With dependent types, not only a function can have the type of its output depend on the value of one of its input, but also pairs of values can become dependently typed, which means that the type of the second component y of a pair (x,y) can depend on the value of the first component. Thesedependent pairsare written(x ** F(x)) in Idris. We 5Imperative and object-oriented languages are now starting to get some of the

can for example define the type of pairs where the first component is a natural number n, and the second a vector of booleans of sizen:

PairNatVect : Type

PairNatVect = (n:Nat ** Vect n Bool)

The value(2 ** [True, True]) is an inhabitant of this type, but

(2 ** [True, True, True]) is not, because the second component has type Vect 3 Bool while it is expected to have typeVect 2 Bool.

As we’ve seen in the previous section with the example of the type

Vect of "lists" indexed by their size, an inductive type can be indexed over a value. This is possible because inductive types are not different from other types in regard to dependent types, and it is therefore possible to inductively define IndexedType of type TypeIndex → Type. If i has type TypeIndex, then(IndexedType i) is a type. Therefore,

IndexedType describes a family of types, indexed overTypeIndex. In practice, in order to define inductively such a family of types, we still use constructors, and each constructor targets a part of the family of types, by specifying what the index is for this constructor. To make things clearer, ifTypeIndexis a type that contains the constantsI1andI2, an indexed type can be defined inductively like this :

data IndexedType : TypeIndex → Type where

Constr1 : ... → indexedType I1

Constr2 : ... → indexedType I2

...

This definition means thatConstr1targets the part that is indexed overI1, i.e. it builds values that have type(indexedType I1), while

Constr2 builds values that have type (indexedType I2).

Dependent types more formally : Π types andΣtypes

We’ve just seen with the function ZeroOrNil that functions can now have the type of their output depend on the values of their inputs. The usual function space (written with the arrow symbol →) with a fixed codomain found in non dependently-typed languages has been replaced by a dependent function space. This has been made possible by several

changes, and the most important of them is that there is no longer a separation between terms and types. In non dependently-typed systems, the relationx:T relates a termxand a typeT, and these two things live at different levels. However, in dependently-typed systems, there is no longer such a strict separation, and everything is a term. That means that types are no longer just a semantic information about a term, but they are ordinary values that can be computed. Being given a type A, we’ve seen that we can write a family of types B:A→Type which assigns to each terma:Aa typeB(a):Type. The functionNatOrListthat we’ve written above was such a family of type. We’ve also seen that we can use such a family of type in order to build a dependent function, i.e. a function where there is no fixed codomain. Formally, the type of such a function is a pi-type, denoted Π(x:a)B(x). In Idris, the type of such a function is

simply written(x:a) → (B x), and the fact that the right-hand side

of the arrow uses the value x makes it explicit that this is a dependent

function. The function ZeroOrNilthat we’ve defined above was such a

dependent function.

When B : A → Type is a constant function, the type Π(x:A)B(x)

becomes completely equivalent to A →B, which means that dependent functions and "ordinary" functions coincide when the type of the output doesn’t change with the value of the input.

In the same way that the ordinary function space has been generalised with pi-types, ordinary pairs are generalised with sigma-types, that are written Σ(x:A)B(x), where Bis still a family of types. For example, the

type of dependent pairs with a natural number n as first component and a vector of booleans of size n as second component –that we’ve defined above in Idris as(n:Nat ** Vect n Bool)– is formally writ- tenΣ(n:Nat)(Vectn Bool) in type theory. This generalisation of pairs is

completely equivalent to ordinary pairs when the type of the second com- ponent does not change with the value of the first : the type Σ(x:A)B(x)