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.