Allow me to supplement Pierre-Marie Pédrot's excellent answer with trivial observations that show very concretely what the problem is.
Consider the following coinductive definitions:
CoInductive serpent : Type := cons : serpent -> serpent.
CoFixpoint jormundgandr := cons jormundgandr.
Is jormundgandr
equal to cons jormundgandr
? Yes, certainly, that's how jormundgandr
is defined. Therefore, we would expect the following to work:
Lemma expected : jormundgandr = cons jormundgandr.
Proof.
reflexivity. (* Does not work. *)
Qed.
It does not work because the normal forms of jormundgandr
and cons jormundgandr
are not syntactically equal:
Eval compute in jormundgandr.
(* output:
= cofix jormundgandr : serpent := cons jormundgandr
: serpent
*)
Eval compute in cons jormundgandr.
(* output:
= cons (cofix jormundgandr : serpent := cons jormundgandr)
: serpent
*)
For some reason Coq does not understand that we could just unfold cofix
once, like this:
jormundgandr ≡
cofix jormundgandr : serpent := cons jormundgandr ≡
cons (cofix jormundgandr : serpent := cons jormundgandr) ≡
cons jormundgandr
We can read the above derivation bottom-up in which case we see that we could fold the cofix once. What would be a general strategy for proving such equalities involving cofix
?
We shouldn't unfold every cofix
blindly because it will go on forever. Coq has a rule: cofix
may only be unfolded if an eliminator (for instance a match
) is applied to it, because the eliminator will immediately consume whatever cofix
produces, thereby guaranteeing progress. (Symmetrically, it is safe to unfold a fix
so long as it is applied to a constructor.)
We could try to fold cofix
when we see an unfolded one, and that would help in very simple cases, but not in general. For example, given
CoFixpoint boa := cons (cons (cons (cons boa))).
CoFixpoint viper := cons (cons (cons (cons (cons (cons boa))))).
how do we fold and unfold to show that boa ≡ viper
? And even if we answer this particular question, we still do not know what to do in general.
We could try all possibilities using some sort of breadth-first search, but that will cause the equality checker to run forever in some cases.
The moral of the story is that there just isn't any good way of dealing with this sort of problem. Because Coq does not unfold cofix
in the most intelligent way possible, it fails to prove some equations that hold in the type theory, which means that completeness of subject reduction is broken in Coq.
Now, let us look at the other possibility:
CoInductive serpent : Type := { cons : serpent }.
CoFixpoint midgar := {| cons := midgar |}.
Lemma expected : midgar = cons midgar.
Proof.
reflexivity. (* It works! *)
Qed.
What's the difference? The normal forms are equal this time:
Eval compute in midgar.
(* output:
= cofix midgar : serpent := {| cons := midgar |}
: serpent
*)
Eval compute in cons midgar.
(* output:
= cofix midgar : serpent := {| cons := midgar |}
: serpent
*)
Coq used the fact that cons
is an elimnator, so it unfolded cofix
, like this:
cons midgar ≡
cons (cofix midgar : serpent := {| cons := midgar |}) ≡
cons {| cons := (cofix midgar : serpent := {| cons := midgar |}) |} ≡
cofix midgar : serpent := {| cons := midgar |}.
(In the case of jormundgandr
the same trick does not work because there cons
was a constructor.)
However, we do have to pay a price. Is midgar
equal to {| cons := midgar |}
? Not by reflexivity:
Lemma expectedToo : midgar = {| cons := midgar |}.
Proof.
reflexivity. (* Does not work. *)
Qed.
This is not failure of subject reduction! The principle that would allow us to prove expectedToo
by reflexivity is known as extensionality for record types. It is not part of Coq's type theory.
Let me just mentioned that extensionality of record types holds for propositional equality, so we have the following:
Lemma loki (s t : serpent) : cons s = cons t -> s = t.
Proof.
case s as [s'].
case t as [t'].
simpl.
intros [].
reflexivity.
Qed.
Lemma expectedToo : midgar = {| cons := midgar |}.
Proof.
now apply loki.
Qed.
However, because this is not a proof by reflexivity, it won't be used automagically by Coq normalization. You will have to apply it manually.
I hope this gives you an idea on how the positive and negative co-inductive types differ, and what the problem with subject reduction is.
Cons_hd_tl
example is very interesting, but I don't understand what specifically about Coq's typing rules allow it to type check or why it immediately causes a problem. (In particular, I don't understand why a stream can be closed given that Coq streams only have one codata constructor) $\endgroup$