I'm trying to learn Coq, but I find it hard to make the leap from what I read in Software Foundations and Certified Programming with Dependent Types to my own use cases.
In particular, I thought I'd try to make a verified version of the nth
function on lists. I managed to write this:
Require Import Arith.
Require Import List.
Import ListNotations.
Lemma zltz: 0 < 0 -> False.
Proof.
intros. contradict H. apply Lt.lt_irrefl.
Qed.
Lemma nltz: forall n: nat, n < 0 -> False.
Proof.
intros. contradict H. apply Lt.lt_n_0.
Qed.
Lemma predecessor_proof: forall {X: Type} (n: nat) (x: X) (xs: list X),
S n < length (x::xs) -> n < length xs.
Proof.
intros. simpl in H. apply Lt.lt_S_n. assumption.
Qed.
Fixpoint safe_nth {X: Type} (n: nat) (xs: list X): n < length xs -> X :=
match n, xs with
| 0, [] => fun pf: 0 < length [] => match zltz pf with end
| S n', [] => fun pf: S n' < length [] => match nltz (S n') pf with end
| 0, x::_ => fun _ => x
| S n', x::xs' => fun pf: S n' < length (x::xs') => safe_nth n' xs' (predecessor_proof n' x xs' pf)
end.
This works, but it raises two questions:
- How would experienced Coq users write this? Are the three lemmas really necessary? Is this a use case for
{ | }
types? - How do I call this function from other code, i.e., how do I supply the required proofs?
I tried this:
Require Import NPeano.
Eval compute in if ltb 2 (length [1; 2; 3]) then safe_nth 2 [1; 2; 3] ??? else 0.
But of course this won't work until I figure out what to write for the ???
part. I tried putting (2 < length [1; 2; 3])
there but that has type Prop
rather than type 2 < length [1; 2; 3]
. I could write and prove a lemma of that specific type, and that works. But what's the general solution?
zltz
has the same type asnltz 0
.To use your function with
2
and[1; 2; 3]
from within another function, you can uselt_dec
.If you extract
lt_dec
, you'll find it's very similar toltb
after the proofs are erased. You don't need to uselt_dec
if you can build your proof from within the function wheresafe_nth
is called.You can shorten your function a bit like this.
I'm not sure what best practices are, but you get tidier extracted code if you use
sig
.I don't think there is a consensus on what the best way for doing this sort of thing is.
I believe that usually Coq developments tend to favor indexed inductive types for writing code like that. This is the solution followed by the vector library in the Coq distribution. There, you would define an indexed inductive type for vectors and another one for bounded integers (called
Vector.t
andFin.t
in the standard library, respectively). Some functions, such asnth
, are much simpler to write in this style, since pattern matching on vectors and indices ends up doing a little bit of reasoning for you when getting rid of contradictory cases and doing recursive calls, for instance. The disadvantage is that dependent pattern matching in Coq is not very intuitive, and sometimes you have to write your functions in a weird way to get them to work. Another problem with this approach is that one needs to redefine many functions that work on lists to work on vectors.Another solution is to define bounded integers as a dependent pair of a
nat
and a proof that that index is bounded, which is essentially what you going for when you mentioned{ | }
types. This is the approach followed by the ssreflect library, for instance (look fot theordinal
type). To define a safenth
function, what they do is to define a simple version that takes a default element to return when the index is out of bounds, and use the proof thatn < length l
to supply that default element (have a look for instance at tuple library of ssreflect, where they define length-indexed lists, and see how they definetnth
). The advantage is that it is easier to relate the more informative types and functions to the simpler variants. The disadvantage is that some things become harder to express directly: for instance, you can't do pattern-matching directly on an ssreflect tuple.Another point worth noting is that very often it is easier to work with boolean properties instead of inductively defined ones, because computation and simplification obviate the need for some lemmas. Thus, when using a boolean version of
<
, Coq doesn't make a difference between a proof of0 < 0 = true
andfalse = true
, or between a proof ofS n < length (x :: l) = true
and a proof ofn < length l = true
, which means that you would be able to use those proofs directly in your definition ofnth
without having to massage them with auxiliary lemmas. The Coq standard library, unfortunately, tends to favor inductively defined types over boolean computations in many cases where they are not useful, such as for defining<
. The ssreflect library, on the other hand, makes much more use of boolean computations for defining properties, making it more appropriate for this style of programming.