• No results found

Strong specification as dependent types

1.2 Dependent types

1.2.3 Strong specification as dependent types

We’ve just seen that in this kind of dependently-typed language, we can write predicates (like ">"), and expressions (like b>0), that help to create pre-conditions that will be verified at compile-time by the ma- chine. The activity of specifying formally the behaviour of a function doesn’t stop here. In fact, we have all the necessary bricks for writing any logical formulae that can be expressed in higher-order logic, which gives us the ability to write post-conditions as well. If one wants to completely specify the behaviour of the previous integer division func- tion, one would usually write the logical formulae ∀a:Nat, ∀b:Nat, ∀p:

(b > 0), (dividea (b,p)) ∗ b ≤a ∧ ((divide a (b,p)) +1) ∗ b > a. This formulae says that the result is such that if we multiply it by the denominator b then we get back at most the numerator a; and also that if this result would be larger by one, then multiplying it by the denominator b would give strictly more than the numerator a. We’ve already mentioned that in a constructive theory, logical formu- lae correspond to types. That means that this logical formulae will become a type, and more precisely the type of a dependent function, as each universal quantification will become a pi-abstraction. It is a dependent type (and not an ordinary function with the usual func- tion space →) because the third argument (the proof p) uses the value

of the second argument (the denominator b), and more significantly because the type of the output (which is divide a (b,p)) ∗ b ≤

a ∧ ((divide a (b,p)) +1) ∗ b > a) uses the values of all the in- puts. Therefore, this logical formulae becomes the following pi-type:

divide_correct : Π(a:Nat) Π(b:Nat) Π(p:GT b0) (divide a(b,p))∗b≤

a∧((divide a (b,p)) +1) ∗ b > a. A proof of it will be any inhabitant of this type, i.e. any dependent function that has this type.

We’ve just encountered the logical side of the dependent function space : pi-types Πx:A(P x) can be read as universal quantifications

∀x: A, P x when they are used to state a logical proposition. There was no existential quantification in this formulae, but we can note that

(∃x:T, P x)would become a sigma-typeΣ(x:T)P x that carries both the

value xand the proof that x is conform, i.e. an inhabitant of(P x). The logical formula that we have expressed above can be expressed as a lemma after the definition of the function. This approach is what we call the "usual approach" for formal certification, where we first write a function, and later complete the type of the function by one or a few additional lemmas, that all together specify completely the behaviour of the function. This is what we’ve done for thelengthfunction for lists, with the lemma length_correctthat we’ve presented in section 1.1.3 and this is what is usually done with the Coq proof assistant. When the behaviour of a function has to be specified by one or more additional lemmas like length_correctanddivide_correct, we say that the

type of the function is a weak type. It is weak in the sense that it is not sufficient for specifying completely the expected behaviour of the function.

However, now that we have dependent types, it becomes possible to write strong types that will specify completely the behaviour of a function. The type becomes of course more complicated, and the same goes for the definition of the function itself, but it has the advantage to ensure from the start that the function being defined is exactly what we need. With the usual approach it was possible to only realise that the function is incorrect when we’re trying to prove its correctness in vain. However, with stronger types, this risk is reduced. In the case ofdivide, a possible

a)∧ ((c+1)∗b > a)). The output is a dependent pair, whose first componentcis the result of the division, and the second is the proof that this result has all the properties that we need in order to consider this function as correct.

We have to be careful with dependent types and strong specification as dependent types, as they can make both types and definitions a lot more complicated and hard to maintain. Putting all the properties directly into the type is not necessarily always the best thing to do. Instead, trying to find some important properties, that are easy enough to express, and powerful enough for ensuring a good level of safety is often more interesting. Dependent types bring the possibility to add logical properties into our types, but we are not limited to have nothing or everything into them, and the difficult but rewarding goal is to find a good compromise. Often, the entire correctness isn’t what really matters. Ensuring some properties or invariants can already give some very strong guarantees and dependent types are very useful for this task.

Let’s recall that everything is not only about post-conditions. There’s also the fact that some functions are not defined if their inputs does not verify some conditions (called pre-conditions), as we have seen with the function dividethat is not defined when the denominator is zero. We can also mention the traditional zip function for lists, because the two lists in input must have the same length. In a more traditional language that does not have dependent types (like Haskell or Ocaml), we usually return an error, raise an exception, or do the treatment partially and as much as possible, stopping when the end of the shortest list is reached. But when working in a dependently-typed theory, we can write the most precise typezip : Vect n A → Vect n B → Vect n (A*B).

Here, the typechecker will (statically) make sure that the two vectors are always forced to have the same size, and it will do that by inspecting their type, as their size is a part of their type.