# Computing with opaque proofs

Posted by Coq. Suggest edits or corrections.

on Wed 17 June 2020 inA fairly common occurrence when working with dependent types in Coq is to call `Compute`

on a benign expression and get back a giant, partially-reduced term, like this:

Import EqNotations Vector.VectorNotations.

This post shows how to work around this issue.

First, a bit of background. In Coq, constants (like theorems and definitions) come in two flavors: *unfoldable* (also known as *transparent*), and *opaque*. This distinction is particularly relevant when performing δ-reduction: transparent constants (introduced using `Definition`

, `Fixpoint`

, `Let`

, etc., or by closing a proof using `Defined`

) can be unfolded, while opaque ones (introduced by closing a proof with `Qed`

) are left untouched (unfolding a constant means replacing its name by its body). In other words, opaque constants are treated very similarly to axioms for computation purposes; according to the manual, …

… this is to keep with the usual mathematical practice of proof irrelevance: what matters in a mathematical development is the sequence of lemma statements, not their actual proofs. This distinguishes lemmas from the usual defined constants, whose actual values are of course relevant in general.

Concretely, here is how the distinction manifests itself:

Definition transparent : nat := 1 + 1.1 + 1 = 2auto. Qed.1 + 1 = 2

This distinction is usually harmless, but it can bite when working with dependent types, which tend to blur the lines between “lemmas” and “defined constants”. Let's look at an example in more detail. Coq's Vector library provides a function `Vector.cons: forall {A}, A -> forall {n}, Vector.t A n -> Vector.t A (1 + n)`

to prepend a single element to a vector, but no corresponding function `snoc`

to *append* a single element to a vector. There *is* a function `Vector.append: forall {A} {n p}, Vector.t A n -> Vector.t A p -> Vector.t A (n + p)`

though, so it's almost enough to define `snoc`

as `fun v a => v ++ [a]`

. If we want `snoc`

to have the same type as `cons`

, we'll just have to add a type cast, because the type of `v ++ [a]`

is `Vector.t nat (n + 1)`

, and `n + 1`

and `1 + n`

are not unifiable. No big deal:

```
Definition snoc {A n} (a: A) (v: Vector.t A n)
: Vector.t A (S n) :=
(* [Nat.add_1_r] has type [forall n : nat, n + 1 = S n] *)
rew (Nat.add_1_r n) in (v ++ [a]).
```

Note

In this definition I used the `rew`

notation (a convenient way to write casts). Here's a version using the `rewrite`

tactic instead:

A:Typen:nata:Av:Vector.t A nVector.t A (S n)A:Typen:nata:Av:Vector.t A nVector.t A (S n)exact (v ++ [a]). Defined.A:Typen:nata:Av:Vector.t A nVector.t A (n + 1)

But now look at what happens if I actually try to compute with this function!

Agh. Could we maybe *prove* that `Vector.map (fun x => 2 * x) (snoc 4 [1; 2; 3])`

equals `[2; 4; 6; 8]`

, instead of using compute?

Vector.map (fun x : nat => 2 * x) (snoc 4 [1; 2; 3]) = [2; 4; 6; 8]Vector.map (fun x : nat => 2 * x) (snoc 4 [1; 2; 3]) = [2; 4; 6; 8]

`reflexivity`

fails, for the same reason that `Compute`

got stuck:

And if I try to simplify things to figure out where I'm stuck, I get exactly where `Compute`

took me before:

Vector.map (fun x : nat => 2 * x) (rew [Vector.t nat] Nat.add_1_r 3 in [1; 2; 3; 4]) = [2; 4; 6; 8]Vector.map (fun x : nat => 2 * x) match Nat.add_1_r 3 in (_ = y) return (Vector.t nat y) with | eq_refl => [1; 2; 3; 4] end = [2; 4; 6; 8]

The problem is that the proof of `Nat.add_1_r`

is opaque, so `cbv`

can't reduce it. If you look carefully at the large term from earlier, you'll see that it, too, was blocked on `(Nat.add_1_r n)`

.

`Abort.`

Finishing the proof without redefining `Nat.add_1_r`

You might think that `destruct`

would help: isn't that how you make a `match`

reduce?

Vector.map (fun x : nat => 2 * x) (snoc 4 [1; 2; 3]) = [2; 4; 6; 8]Vector.map (fun x : nat => 2 * x) (snoc 4 [1; 2; 3]) = [2; 4; 6; 8]Vector.map (fun x : nat => 2 * x) match Nat.add_1_r 3 in (_ = y) return (Vector.t nat y) with | eq_refl => [1; 2; 3; 4] end = [2; 4; 6; 8]Abort.

… apparently not. When you first see this error, it usually feels a bit disorienting. We can get a bit closer by swapping the rewrite and the map:

A, B:Typen:natforall (v : Vector.t A n) (f : A -> B) (a : A), Vector.map f (snoc a v) = snoc (f a) (Vector.map f v)A, B:Typen:natforall (v : Vector.t A n) (f : A -> B) (a : A), Vector.map f (snoc a v) = snoc (f a) (Vector.map f v)A, B:Typen:natv:Vector.t A nf:A -> Ba:AVector.map f (rew [Vector.t A] Nat.add_1_r n in (v ++ [a])) = rew [Vector.t B] Nat.add_1_r n in (Vector.map f v ++ [f a])A, B:Typen:natv:Vector.t A nf:A -> Ba:AVector.map f (rew [Vector.t A] eq_refl in (v ++ [a])) = rew [Vector.t B] eq_refl in (Vector.map f v ++ [f a])

(A puzzle: why did *this* destruct work, unlike the previous one?)

A, B:Typen:natv:Vector.t A nf:A -> Ba:AVector.map f (v ++ [a]) = Vector.map f v ++ [f a]reflexivity. Qed.A, B:Typen:natv:Vector.t A nf:A -> Ba:AVector.map f v ++ Vector.map f [a] = Vector.map f v ++ [f a]

With this, we're tantalizingly close:

Vector.map (fun x : nat => 2 * x) (snoc 4 [1; 2; 3]) = [2; 4; 6; 8]Vector.map (fun x : nat => 2 * x) (snoc 4 [1; 2; 3]) = [2; 4; 6; 8]snoc (2 * 4) (Vector.map (fun x : nat => 2 * x) [1; 2; 3]) = [2; 4; 6; 8]match Nat.add_1_r 3 in (_ = y) return (Vector.t nat y) with | eq_refl => [2; 4; 6; 8] end = [2; 4; 6; 8]

… agh! A useless rewrite! Get it off!

… agh!

Turns out you need a pretty powerful theorem to get yourself out of this mess. Because equality on natural numbers is decidable, it's possible to prove that there's just one proof of `4 = 4`

, namely `eq_refl`

. With that proof, we can rewrite `(Nat.add_1_r 3)`

into `eq_refl`

:

reflexivity. Qed.[2; 4; 6; 8] = [2; 4; 6; 8]

Of course, that doesn't help with making `Compute`

reduce, since `Compute`

applies a limited set of reduction rules (it doesn't do rewrites).

The usual fix is to make every proof that might be used in a computation transparent; something like this:

n:natn + 1 = S nn:natn + 1 = S n0 + 1 = 1n:natIHn:n + 1 = S nS n + 1 = S (S n)reflexivity.0 + 1 = 1n:natIHn:n + 1 = S nS n + 1 = S (S n)n:natIHn:n + 1 = S nS (n + 1) = S (S n)reflexivity. Defined. (* [Defined] makes the proof transparent *)n:natIHn:n + 1 = S nS (S n) = S (S n)

Equivalently, we can write the proof as a recursive function, which makes the fact that it always reduces to `eq_refl`

pretty obvious (the second match could also be written as `rew [fun y => S n + 1 = S y] (add_1_r_transparent n) in eq_refl`

):

```
Fixpoint add_1_r_transparent (n: nat)
: n + 1 = S n :=
match n with
| 0 => eq_refl
| S n =>
match add_1_r_transparent n in (_ = y)
return (S n + 1 = S y) with
| eq_refl => eq_refl
end
end.
```

… and with this, we can get a definition that computes properly:

```
Definition snoc_computational {A n} (a: A) (v: Vector.t A n)
: Vector.t A (S n) :=
rew (add_1_r_transparent n) in (v ++ [a]).
```

The problem with this approach is that this change needs to be done recursively: you would need to replace `Qed`

with `Defined`

in the definition of all lemmas that you depend on. Not ideal.

Instead, here's a cool trick:

```
Definition computational_eq {m n} (opaque_eq: m = n) : m = n :=
match Nat.eq_dec m n with
| left transparent_eq => transparent_eq
| _ => opaque_eq (* dead code; could use [False_rect] *)
end.
```

```
Definition snoc' {A n} (a: A) (v: Vector.t A n) :=
rew (computational_eq (Nat.add_1_r n)) in (v ++ [a]).
```

🎉️. Here is why this works: in the definition of `snoc`

, all that I really need is to have *some* transparent proof that `n + 1`

equals `1 + n`

— I don't really care which one. So, because equality is decidable on `nat`

, I can just ignore the opaque proof returned by `Nat.add_1_r`

, and rebuild a fresh, transparent one using `Nat.eq_dec`

.

More precisely, for any given `m`

and `n`

, one way to construct a proof that `m = n`

is to compare the two numbers, using a recursive comparison function (that's what `Nat.eq_dec`

is, above; importantly, it returns transparent proofs). If the comparison succeeds, it returns a transparent equality proof (`eq_refl`

) that `computational_eq`

returns, allowing computation to proceed.

For arbitrary `m`

and `n`

, values, the comparison could fail. But in `computational_eq`

, it can't: `computational_eq`

takes a proof that its first two arguments are equal, so the second branch of the match is dead code, and the opaque proof isn't computationally relevant anymore.

The key here is that `Nat.eq_dec`

builds a transparent proof, so there's really nothing magical happening — all we're doing is discarding potentially opaque equality proofs on `nat`

s and replacing them with a canonical one that we know to be transparent. The big gain is that we only have to define `Nat.eq_dec`

once, instead of having to make all equality proofs transparent.

Note

This issue (computations blocking on equality proofs) highlights one way in which `eq`

(and other singleton inductives in Prop) are special in Coq. If you think about it, it's a bit weird that we even run into this problem at all: since Coq's `Prop`

s are erasable, shouldn't be computationally irrelevant? If so, how can they block computation?

It turns out that singleton (and empty) definitions are `special-cased <https://coq.inria.fr/refman/language/cic.html#empty-and-singleton-elimination>`

, because they allow useful patterns like typecasts without breaking `Prop`

erasure or extraction. In constrast, Coq prevents other `Prop`

s from being used in any context except to derive a contradiction (that is, as an argument to `False_rect`

): that's why the problem only pops up with `eq`

and similar `Prop`

s, and why the following example computes without blocking (it never really uses the proof, in the same way that `computational_eq`

above never really uses its argument proof):

Computational complexity

There's something slightly worrying about discarding old proofs to build new ones — aren't we going to be wasting cycles building those new proofs?

In most cases, not really: `Nat.eq_dec`

is linear in its first argument, so it could be slow, but so is our transparent replacement for `Nat.add_1_r`

! (`add_1_r_transparent`

builds a trivial term, `eq_refl`

, but it builds it recursively, traversing its whole argument.)

Worse, if the proof that we're discarding is built from complex lemmas, discarding it instead of normalizing a transparent variant of it can save quite a bit of time.

Ultimately, it depends on the reduction strategy, on the actual proof that we're discarding, and on its opacity. If we reduce in lazy or call-by-need style, the original proof won't be reduced at all. In call-by-value style, however, we can run into issues: the reduction of `computational_eq pr`

will start by reducing `pr`

, so in the example above we'll normalize *both* `Nat.eq_dec`

and the original proof passed to `computational_eq`

. There are two workarounds:

Thunk the original proof (that is, wrap it in a function):

`cbv`

only reduces arguments to weak-head normal form, so the thunk won't be reduced).`Definition computational_eq_cbv {m n: nat} (thunked_eq: unit -> m = n) : m = n := match Nat.eq_dec m n with | left transparent_eq => transparent_eq | _ => thunked_eq tt end.`

Make the original proof opaque. Ironically, since opacity blocks reduction, the normal form of

`Nat.add_1_r`

is precisely`Nat.add_1_r`

, so we won't run into the double-reduction issue as long as the proof we're passing to`computational_eq`

is opaque.

When extracting to OCaml, none of this matters: `computational_eq`

is erased: