Your intuition for bool_ind
is spot on, but thinking about why bool_ind
means what you said might help clarify the other two. We know that
bool_ind : forall P : bool -> Prop,
P true ->
P false ->
forall b : bool,
P b
If we read this as a logical formula, we get the same reading you did:
- For every predicate
P
on bool
eans,
- If
P true
holds, and
- If
P false
holds, then
- For every boolean
b
,
But this isn't just a logical formula, it's a type. Specifically, it's a (dependent) function type. And as a function type, it says (if you'll allow me the liberty of inventing names for the unnamed arguments and the result):
- Given a value
P : bool -> Prop
,
- A value
Pt : P true
,
- A value
Pf : P false
, and
- A value
b : bool
,
- We can construct a value
Pb : P b
.
(Of course, this is a curried function, so there are other ways to break down the type into prose, but this is clearest for our purposes.)
The big important thing here, the thing that makes Coq work as a theorem prover while being a programming language (or vice versa) is the Curry-Howard correspondence: types are propositions, and values are proofs of those propositions. For instance, the simple function type ->
corresponds to implication, and the dependent function type forall
corresponds to universal quantification. (The notation is pretty suggestive :-)) So in Coq, to prove that φ → ψ, we must construct a value of type φ -> ψ
: a function that takes a value of type φ
(or in other words, a proof of the proposition φ) and uses it to construct a value of type ψ
(a proof of the proposition ψ).
In Coq, we can think about all types in this way, whether those types live in Set
, Type
, or Prop
. (So when you say "It seems as if P true (which is a Set for bool rec and a Type for bool_rect) is being treated as a propositional value," you're right!) For instance, let's consider how we'd implement bool_ind
ourselves. We'll start by listing all the parameters to the function, along with its return type:
Definition bool_ind' (P : bool -> Prop)
(Pt : P true)
(Pf : P false)
(b : bool)
: P b :=
So far, so good. At this point, we'd like to return something of type P b
, but we don't know what b
is. So, as always in these situations, we pattern match:
match b with
There are now two cases. First, b
could be true
. In this case, we must want to return something of type P true
, and luckily we have such a value: Pt
.
| true => Pt
The false
case is similar:
| false => Pf
end.
Note that when we implement bool_ind'
, it doesn't look very "proofy," but rather very "programmy". Of course, thanks to the Curry-Howard correspondence, these are the same. But note that the very same implementation will suffice for the other two functions:
Definition bool_rec' (P : bool -> Set)
(Pt : P true)
(Pf : P false)
(b : bool)
: P b :=
match b with
| true => Pt
| false => Pf
end.
Definition bool_rect' (P : bool -> Type)
(Pt : P true)
(Pf : P false)
(b : bool)
: P b :=
match b with
| true => Pt
| false => Pf
end.
Looking at this computational definition exposes another way to thing about bool_ind
, bool_rec
, and bool_rect
: they encapsulate what you need to know to talk about every value of bool
. But either way, we're packaging up that information: if I know something for true
, and something for false
, then I know it for all the bool
s.
The definition of the bool_{ind,rec,rect}
functions abstracts over the usual way we write functions on booleans: there's one argument corresponding to the true branch, and one to the false branch. Or, in other words: these functions are just if
statements. In a non–dependently-typed language, they could have the simpler type forall S : Set, S -> S -> bool -> S
:
Definition bool_simple_rec (S : Set) (St : P) (Sf : P) (b : bool) : S :=
match b with
| true => St
| false => Sf
end.
However, because types can depend on values, we must thread the b
through the types everywhere. If it turns out we don't want that, though, we can use our more general function and tell :
Definition bool_simple_rec' (S : Set) : S -> S -> bool -> S :=
bool_rec (fun _ => S).
Nobody ever said our P : bool -> Set
had to use the bool
!
All of these functions are a lot more interesting for recursive types. For instance, Coq has the following type of natural numbers:
Inductive nat : Set := O : nat | S : nat -> nat.
And we have
nat_ind : forall P : nat -> Prop,
P O ->
(forall n' : nat, P n' -> P (S n')) ->
forall n : nat,
P n
Along with the corresponding nat_rec
and nat_rect
. (Exercise for the reader: implement these functions directly.)
At first glance, this is just the principle of mathematical induction. However, it's also how we write recursive functions on nat
s; they're the same thing. In general, recursive functions over nat
look like the following:
fix f n => match n with
| O => ...
| S n' => ... f n' ...
end
The arm of the match following O
(the base case) is just the value of type P O
. The arm of the match following S n'
(the recursive case) is what's passed into the function of type forall n' : nat, P n' -> P (S n')
: the n'
s are the same, and the value of P n'
is the result of the recursive call f n'
.
Another way to think about the equivalence between the _rec
and _ind
functions, then—and one which I think is clearer on infinite types than on bool
—is that it's the same as the equivalence between mathematical ind
uction (which happens in Prop
) and (structural) rec
ursion (which happens in Set
and Type
).
Let's get praxic and use these functions. We'll define a simple function that converts booleans to natural numbers, and we'll do it both directly and with bool_rec
. The simplest way to write this function is with a pattern match:
Definition bool_to_nat_match (b : bool) : nat :=
match b with
| true => 1
| false => 0
end.
The alternative definition is
Definition bool_to_nat_rec : bool -> nat :=
bool_rec (fun _ => nat) 1 0.
And these two functions are the same:
Goal bool_to_nat_match = bool_to_nat_rec.
Proof. reflexivity. Qed.
(Note: these functions are syntactically equal. This is a stronger condition than simply doing the same thing.)
Here, the P : bool -> Set
is fun _ => nat
; it gives us the return type, which isn't dependent on the argument. Our Pt : P true
is 1
, the thing to compute when we're given true
; similarly, our Pf : P false
is 0
.
If we want to use the dependency, we have to cook up a useful data type. How about
Inductive has_if (A : Type) : bool -> Type :=
| has : A -> has_if A true
| lacks : has_if A false.
With this definition, has_if A true
is isomorphic to A
, and has_if A false
is isomorphic to unit
. We could then have a function which retains its first argument if and only if it's passed true
.
Definition keep_if_match' (A : Type) (a : A) (b : bool) : has_if A b :=
match b with
| true => has A a
| false => lacks A
end.
The alternative definition is
Definition keep_if_rect (A : Type) (a : A) : forall b : bool, has_if A b :=
bool_rect (has_if A) (has A a) (lacks A).
And they're again the same:
Goal keep_if_match = keep_if_rect.
Proof. reflexivity. Qed.
Here, the return type of the function is dependent upon the argument b
, so our P : bool -> Type
actually does something.
Here's a more interesting example, using natural numbers and length-indexed lists. If you haven't seen length-indexed lists, also called vectors, they're exactly what they say on the tin; vec A n
is a list of n
A
s.
Inductive vec (A : Type) : nat -> Type :=
| vnil : vec A O
| vcons : forall n, A -> vec A n -> vec A (S n).
Arguments vnil {A}.
Arguments vcons {A n} _ _.
(The Arguments
machinery handles implicit arguments.) Now, we want to produce a list of n
copies of some particular element, so we can write that with a fixpoint:
Fixpoint vreplicate_fix {A : Type} (n : nat) (a : A) : vec A n :=
match n with
| O => vnil
| S n' => vcons a (vreplicate_fix n' a)
end.
Alternatively, we can use nat_rect
:
Definition vreplicate_rect {A : Type} (n : nat) (a : A) : vec A n :=
nat_rect (vec A) vnil (fun n' v => vcons a v) n.
Note that since nat_rect
captures the recursion pattern, vreplicate_rect
is not a fixpoint itself. One thing to note is the third argument to nat_rect
:
fun n' v => vcons a v
The v
there is conceptually the result of the recursive call to vreplicate_rect n' a
; nat_rect
abstracts out that recursion pattern, so we don't need to call it directly. The n'
is indeed the same n'
as in vreplicate_fix
, but now it seems like we don't need to mention it explicitly. Why is it passed in? That becomes clear if we write out our types:
fun (n' : nat) (v : vec A n') => vcons a v : vec A (S n')
We need n'
so we know what type v
has, and consequently what type the result has.
Let's see these functions in action:
Eval simpl in vreplicate_fix 0 tt.
Eval simpl in vreplicate_rect 0 tt.
(* both => = vnil : vec unit 0 *)
Eval simpl in vreplicate_fix 3 true.
Eval simpl in vreplicate_rect 3 true.
(* both => = vcons true (vcons true (vcons true vnil)) : vec bool 3 *)
And indeed, they're the same:
(* Note: these two functions do the same thing, but are not syntactically
equal; the former is a fixpoint, the latter is a function which returns a
fixpoint. This sort of equality is all you generally need in practice. *)
Goal forall (A : Type) (a : A) (n : nat),
vreplicate_fix n a = vreplicate_rect n a.
Proof. induction n; [|simpl; rewrite IHn]; reflexivity. Qed.
Above, I posed the exercise of reimplementing nat_rect
and friends. Here's the answer:
Fixpoint nat_rect' (P : nat -> Type)
(base_case : P 0)
(recurse : forall n', P n' -> P (S n'))
(n : nat)
: P n :=
match n with
| O => base_case
| S n' => recurse n' (nat_rect' P base_case recurse n')
end.
This hopefully makes it clear just how nat_rect
abstracts the recursion pattern, and why it's sufficiently general.