Reasoning about data¶
Logic tells you how to combine facts; the data modules tell you how to reason about values. Each one hands you two kinds of rule:
case-analysis rules that mirror a type’s constructors — to prove something about any value, prove it for each way the value could have been built;
equations (axioms) describing what the operations do, which you drive with
rewrite_r/rewrite_l(see Rewriting with a motive).
We met one already — induction in nat.alg is exactly the case-analysis rule
for the naturals (base case 0, step case s(n)). Everything here is the same
idea for other shapes.
Pairs and sums¶
adt.alg defines Pair and Sum. A pair is built exactly one way — with
pair — so pair_cases has a single case (and a single premise, hence
then):
import adt(pair_cases, refl);
lemma a_pair_is_itself(A B : Sort, p : Pair(A, B))
⊢ p = p;
proof
by pair_cases(A, B, p, _ = _)
then x : A, y : B ⊢ pair(x, y) = pair(x, y);
by refl(Pair(A, B), pair(x, y));
qed;
pair_cases replaces the opaque p with a concrete pair(x, y) for fresh
x, y. A sum, by contrast, is built two ways — inl or inr — so
sum_cases gives you two branches:
import adt(sum_cases, refl);
lemma a_sum_is_itself(A B : Sort, s : Sum(A, B))
⊢ s = s;
proof
by sum_cases(A, B, s, _ = _) cases
case
x : A;
⊢ inl(x) = inl(x);
by refl(Sum(A, B), inl(x));
qed;
case
y : B;
⊢ inr(y) = inr(y);
by refl(Sum(A, B), inr(y));
qed;
qed;
qed;
That last argument, _ = _, is the motive again — λ k. k = k — the property
being proved of the whole value.
Options, results, lists¶
The data types follow suit. option.alg gives option_cases (none or
some(x)):
import option(option_cases, refl);
lemma an_option_is_itself(A : Sort, m : Option(A))
⊢ m = m;
proof
by option_cases(A, m, _ = _) cases
case
⊢ none = none;
by refl(None, none);
qed;
case
x : A;
⊢ some(x) = some(x);
by refl(Option(A), some(x));
qed;
qed;
qed;
result.alg mirrors it with result_cases (ok(x) or err(e)), and
list.alg gives list_induction — a recursive case analysis, like nat:
the cons case even hands you an induction hypothesis ih about the tail.
import list(list_induction, refl);
lemma a_list_is_itself(A : Sort, xs : List(A))
⊢ xs = xs;
proof
by list_induction(A, xs, _ = _) cases
case
⊢ nil = nil;
by refl(List(A), nil);
qed;
case
x : A;
rest : List(A);
ih := rest = rest;
⊢ cons(x, rest) = cons(x, rest);
by refl(List(A), cons(x, rest));
qed;
qed;
qed;
The equations¶
Case rules take values apart; the equation axioms say what the operations
compute to. option.alg’s bind_some is a fact you can apply directly — it says
binding into a some just runs the function:
import option;
lemma bind_runs_the_function(A B : Sort, x : A, f : A → Option(B))
⊢ bind(some(x), f) = f(x);
proof
by bind_some(A, B, x, f);
qed;
That’s the same move the monad-law proofs in Theories, laws, and models were built from:
case-split with option_cases, then rewrite with bind_none / bind_some
until both sides meet. Every data module is this pair — constructors to split on,
equations to rewrite with.
Your turn
Binding into none throws the function away. Prove it — the axiom you need is
bind_none.
import option;
lemma bind_of_none(A B : Sort, g : A → Option(B))
⊢ bind(none, g) = none;
proof
by wip(?goal);
wip;
Hint
bind_none(A, B, g) proves bind(none, g) = none outright — it’s a
premise-free fact, so a single by bind_none(A, B, g); closes the goal, no
then needed.
That’s the tour. You’ve now met the whole vocabulary: equality and rewriting, the
logical connectives, the quantifiers, and case analysis over every data type in the
library. Everything else in Algae — the option/list/result monad
proofs, the group hierarchy, whatever you build next — is these same rules,
chained a little longer. Open the modules, read a proof, and try to reprove it
yourself. The kernel is patient, and now, so are you.