Implementing Strongly-Specified Functions with the refine
Tactic
Revisions
This revisions table has been automatically generated
from the
git
history of this website repository, and the
change descriptions may not always be as useful as they should.
You can consult the source of this file in its current version here.
2021-03-28 | 2021 Spring redesign | 495f9db |
2020-10-30 | Reword the titles of the “Strongly-Specified Functions”’s articles | 751a595 |
2020-10-10 | Create a summary page for the strongly-specified functions series | b4466fd |
2020-07-14 | Prepare the introduction of a RSS feed | c62a61d |
2020-02-23 | Give up on clean URLs | fbe5ef4 |
2020-02-19 | Do not use raw HTML for the titles of Coq posts | 9d8fc20 |
2020-02-18 | Do not extract Coq terms in Strongly-Specified Functions part 2 | 6263c7c |
2020-02-16 | Automatically generate a revision table from git history | b47ee36 |
2020-02-14 | Fix spelling error | 3d46907 |
2020-02-14 | Rework the introduction of the strongly-specified functions serie | e147992 |
2020-02-04 | Change the date format | 1f851ca |
2020-02-04 | Try to avoid long lines in StronglySpecifiedFunctions.v | 4812baa |
2020-02-04 | Initial commit with previous content and a minimal theme | 9754a53 |
Is this list empty? §
It's the first question to deal with when manipulating
lists. There are some functions that require their arguments not
to be empty. It's the case for the pop function, for instance:
it is not possible to remove the first element of a list that does
not have any elements in the first place.
When one wants to answer such a question as “Is this list empty?”,
he has to keep in mind that there are two ways to do it: by a
predicate or by a boolean function. Indeed, Prop and bool are
two different worlds that do not mix easily. One solution is to
write two definitions and to prove their equivalence. That is
forall args, predicate args <-> bool_function args = true.
Another solution is to use the sumbool type as middleman. The
scheme is the following:
Defining the
A list is empty if it is [] (nil). It's as simple as that!
- Defining predicate : args → Prop
- Defining predicate_dec : args -> { predicate args } + { ~predicate args }
- Defining predicate_b:
Definition predicate_b (args) := if predicate_dec args then true else false.
Defining the empty
predicate §
Defining a decidable version of empty
§
Definition empty_dec {a} (l : list a)
: { empty l } + { ~ empty l }.
Proof.
refine (match l with
| [] => left _ _
| _ => right _ _
end);
unfold empty; trivial.
unfold not; intro H; discriminate H.
Defined.
In this example, I decided to use the refine tactic which is
convenient when we manipulate the Set and Prop sorts at the
same time.
Defining
With empty_dec, we can define empty_b.
Defining empty_b
§
Let's try to extract empty_b:
In addition to
There are several ways to write a function that removes the first
element of a list. One is to return `nil` if the given list was
already empty:
type bool = | True | False type sumbool = | Left | Right type 'a list = | Nil | Cons of 'a * 'a list (** val empty_dec : 'a1 list -> sumbool **) let empty_dec = function | Nil -> Left | Cons (a, l0) -> Right (** val empty_b : 'a1 list -> bool **) let empty_b l = match empty_dec l with | Left -> True | Right -> False
list 'a
, Coq has created the sumbool
and
bool
types and empty_b is basically a translation from the
former to the latter. We could have stopped with empty_dec, but
Left and Right are less readable that True and False. Note
that it is possible to configure the Extraction mechanism to use
primitive OCaml types instead, but this is out of the scope of
this article.
Defining some utility functions §
Defining pop §
But it's not really satisfying. A `pop` call over an empty list
should not be possible. It can be done by adding an argument to
`pop`: the proof that the list is not empty.
There are, as usual when it comes to lists, two cases to
consider.
The challenge is to convince Coq that our reasoning is
correct. There are, again, several approaches to achieve that. We
can, for instance, use the refine tactic again, but this time we
need to know a small trick to succeed as using a “regular” match
will not work.
From the following goal:
Using the refine tactic naively, for instance this way:
- l = x :: rst, and therefore pop (x :: rst) h is rst
- l = [], which is not possible since we know l is not empty.
a : Type l : list a h : ~ empty l ============================ list a
leaves us the following goal to prove:
Nothing has changed! Well, not exactly. See, refine has taken
our incomplete Gallina term, found a hole, done some
type-checking, found that the type of the missing piece of our
implementation is list a and therefore has generated a new
goal of this type. What refine has not done, however, is
remember that we are in the case where l = []!
We need to generate a goal from a hole wherein this information is
available. It is possible using a long form of match. The
general approach is this: rather than returning a value of type
list a, our match will return a function of type l = ?l' ->
list a, where ?l is value of l for a given case (that is,
either x :: rst or []). Of course, As a consequence, the type
of the match in now a function which awaits a proof to return
the expected result. Fortunately, this proof is trivial: it is
eq_refl.
a : Type l : list a h : ~ empty l ============================ list a
refine (match l as l'
return l = l' -> list a
with
| _ :: rst => fun _ => rst
| [] => fun equ => _
end eq_refl).
For us to conclude the proof, this is way better.
We conclude the proof, and therefore the definition of pop.
a : Type l : list a h : ~ empty l equ : l = [] ============================ list a
rewrite equ in h.
exfalso.
now apply h.
Defined.
It's better and yet it can still be improved. Indeed, according to its type,
pop returns “some list”. As a matter of fact, pop returns “the
same list without its first argument”. It is possible to write
such precise definition thanks to sigma-types, defined as:
Rather that sig A p, sigma-types can be written using the
notation { a | P }. They express subsets, and can be used to constraint
arguments and results of functions.
We finally propose a strongly-specified definition of pop.
Inductive sig (A : Type) (P : A -> Prop) : Type := exist : forall (x : A), P x -> sig P.
If you think the previous use of match term was ugly, brace yourselves.
refine (match proj1_sig l as l'
return proj1_sig l = l'
-> { l' | exists a, proj1_sig l = cons a l' }
with
| [] => fun equ => _
| (_ :: rst) => fun equ => exist _ rst _
end eq_refl).
This leaves us two goals to tackle.
First, we need to discard the case where l is the empty list.
a : Type l : {l : list a | ~ empty l} equ : proj1_sig l = [] ============================ {l' : list a | exists a0 : a, proj1_sig l = a0 :: l'}
+ destruct l as [l nempty]; cbn in *.
rewrite equ in nempty.
exfalso.
now apply nempty.
Then, we need to prove that the result we provide (rst) when the
list is not empty is correct with respect to the specification of
pop.
a : Type l : {l : list a | ~ empty l} a0 : a rst : list a equ : proj1_sig l = a0 :: rst ============================ exists a1 : a, proj1_sig l = a1 :: rst
+ destruct l as [l nempty]; cbn in *.
rewrite equ.
now exists a0.
Defined.
Let's have a look at the extracted code:
If one tries to call pop nil, the assert ensures the call fails. Extra
information given by the sigma-type have been stripped away. It can be
confusing, and in practice it means that, we you rely on the extraction
mechanism to provide a certified OCaml module, you cannot expose
strongly-specified functions in its public interface because nothing in the
OCaml type system will prevent a miseuse which will in practice leads to an
It is possible to specify push the same way pop has been. The only
difference is push accepts lists with no restriction at all. Thus, its
definition is a simpler, and we can write it without refine.
(** val pop : 'a1 list -> 'a1 list **) let pop = function | Nil -> assert false (* absurd case *) | Cons (a, l0) -> l0
assert false
.
Defining push §
And the extracted code is just as straightforward.
Same as pop and push, it is possible to add extra information in the
type of head, namely the returned value of head is indeed the firt value
of l.
let push l a = Cons (a, l)
Defining head §
It's not a surprise its definition is very close to pop.
refine (match proj1_sig l as l'
return proj1_sig l = l' -> _
with
| [] => fun equ => _
| x :: _ => fun equ => exist _ x _
end eq_refl).
The proof are also very similar, and are left to read as an exercise for
passionate readers.
+ destruct l as [l falso]; cbn in *.
rewrite equ in falso.
exfalso.
now apply falso.
+ exists l0.
now rewrite equ.
Defined.
Finally, the extracted code is as straightforward as it can get.
Writing strongly-specified functions allows for reasoning about the result
correctness while computing it. This can help in practice. However, writing
these functions with the refine tactic does not enable a very idiomatic
Coq code.
To improve the situation, the
let head = function | Nil -> assert false (* absurd case *) | Cons (a, l0) -> a
Conclusion & Moving Forward §
Program
framework distributed with the Coq
standard library helps, but it is better to understand what Program
achieves
under its hood, which is basically what we have done in this article.