lthms' avatar, a hand drawing looking person, wearing a headset, close to a window on a raining night
Thomas Letan
lthms · he/him

Did you come across something which caught your attention? Don’t hesitate to shoot me an email in my public inbox.

 Published on  Modified on

Proving Algebraic Datatypes are “Algebraic”

 coq

Several programming languages allow programmers to define (potentially recursive) custom types, by composing together existing ones. For instance, in OCaml, one can define lists as follows:

type 'a list =
| Cons of 'a * 'a list
| Nil

This translates in Haskell as

data List a =
  Cons a (List a)
| Nil

In Rust as

enum List<A> {
  Cons(A, Box<List<a>>),
  Nil,
}

Or in Coq as

Inductive list a :=
| cons : a -> list a -> list a
| nil

And so forth.

Each language will have its own specific constructions, and the type systems of OCaml, Haskell, Rust and Coq —to only cite them— are far from being equivalent. That being said, they often share a common “base formalism,” usually (and sometimes abusively) referred to as algebraic datatypes. This expression is used because under the hood any datatype can be encoded as a composition of types using two operators: sum (++) and product (*) for types.

For an algebraic datatype, one constructor allows for defining “named tuples,” that is ad hoc product types. Besides, constructors are mutually exclusive: you cannot define the same term using two different constructors. Therefore, a datatype with several constructors is reminiscent of a disjoint union. Coming back to the list\mathrm{list} type, under the syntactic sugar of algebraic datatypes, we can define it as

listαunit+αlistα\mathrm{list}_\alpha \equiv \mathrm{unit} + \alpha * \mathrm{list}_\alpha

where unit\mathrm{unit} models the nil case, and αlistαα * \mathrm{list}_\alpha models the cons case.

The set of types which can be defined in a language together with ++ and * form an “algebraic structure” in the mathematical sense, hence the name. It means the definitions of ++ and * have to satisfy properties such as commutativity or the existence of neutral elements. In this article, we will prove some of them in Coq. More precisely,

For the record, the sum (++) and prod (*) types are defined in Coq as follows:

Inductive sum (A B : Type) : Type :=
| inl : A -> sum A B
| inr : B -> sum A B

Inductive prod (A B : Type) : Type :=
| pair : A -> B -> prod A B

An Equivalence for Type

Algebraic structures come with equations expected to be true. This means there is an implicit dependency which is —to my opinion— too easily overlooked: the definition of ==. In Coq, = is a built-in relation that states that two terms are “equal” if they can be reduced to the same “hierarchy” of constructors. This is too strong in the general case, and in particular for our study of algebraic structures of Type. It is clear that, to Coq’s opinion, α+β\alpha + \beta is not structurally equal to β+α\beta + \alpha, yet we will have to prove they are “equivalent.”

Introducing type_equiv

Since = for Type is not suitable for reasoning about algebraic datatypes, we introduce our own equivalence relation, denoted ==. We say two types α\alpha and β\beta are equivalent up to an isomorphism when for any term of type α\alpha, there exists a counterpart term of type β\beta and vice versa. In other words, α\alpha and β\beta are equivalent if we can exhibit two functions ff and gg such that:

(x:α), x=g(f(x))\forall (x : α),\ x = g(f(x))

(y:β), y=f(g(y))\forall (y : β),\ y = f(g(y))

This translates into the following inductive type.

Reserved Notation "x == y" (at level 72).

Inductive type_equiv (α β : Type) : Prop :=
| mk_type_equiv (f : α -> β) (g : β -> α)
                (equ1 : forall (x : α), x = g (f x))
                (equ2 : forall (y : β), y = f (g y))
  : α == β
where "x == y" := (type_equiv x y).

As mentioned earlier, we prove two types are equivalent by exhibiting two functions, and proving these functions satisfy two properties. We introduce a Ltac notation to that end.

Tactic Notation "equiv" "with" uconstr(f) "and" uconstr(g)
  := apply (mk_type_equiv f g).

The tactic equiv with f and g will turn a goal of the form α == β into two subgoals to prove f and g form an isomorphism.

type_equiv is an Equivalence

We can prove it by demonstrating it is

  1. Reflexive,
  2. Symmetric, and
  3. Transitive.

type_equiv is reflexive

This proof is straightforward. A type α\alpha is equivalent to itself because:

(x:α), x=id(id(x))\forall (x : α),\ x = id(id(x))

Lemma type_equiv_refl (α : Type) : α == α.
Proof.
  now equiv with (@id α) and (@id α).
Qed.

type_equiv is symmetric

If α=β\alpha = \beta, then we know there exists two functions ff and gg which satisfy the expected properties. We can “swap” them to prove that β==α\beta == \alpha.

Lemma type_equiv_sym {α β} (equ : α == β) : β == α.
Proof.
  destruct equ as [f g equ1 equ2].
  now equiv with g and f.
Qed.

type_equiv is transitive

If α=β\alpha = \beta, we know there exists two functions fαf_\alpha and gβg_\beta which satisfy the expected properties of type_equiv. Similarly, because β=γ\beta = \gamma, we know there exists two additional functions fβf_\beta and gγg_\gamma. We can compose these functions together to prove α=γ\alpha = \gamma.

As a reminder, composing two functions ff and gg (denoted by f >>> g thereafter) consists in using the result of ff as the input of gg.

Infix ">>>" := (fun f g x => g (f x)) (at level 70).

Lemma type_equiv_trans {α β γ} (equ1 : α == β) (equ2 : β == γ)
  : α == γ.
Proof.
  destruct equ1 as [fα gβ equαβ equβα],
           equ2 as [fβ gγ equβγ equγβ].
  equiv with (fα >>> fβ) and (gγ >>> gβ).
  + intros x.
    rewrite <- equβγ.
    now rewrite <- equαβ.
  + intros x.
    rewrite <- equβα.
    now rewrite <- equγβ.
Qed.

Conclusion

The Coq standard library introduces the Equivalence type class. We can provide an instance of this type class for type_equiv, using the three lemmas we have proven in this section.

#[refine]
Instance type_equiv_Equivalence : Equivalence type_equiv :=
  {}.

Proof.
  + intros x.
    apply type_equiv_refl.
  + intros x y.
    apply type_equiv_sym.
  + intros x y z.
    apply type_equiv_trans.
Qed.

Examples

list’s canonical form

We now come back to our initial example, given in the introduction of this write-up. We can prove our assertion, that is list α == unit + α * list α.

Lemma list_equiv (α : Type)
  : list α == unit + α * list α.

Proof.
  equiv with (fun x => match x with
                       | [] => inl tt
                       | x :: rst => inr (x, rst)
                       end)
         and (fun x => match x with
                       | inl _ => []
                       | inr (x, rst) => x :: rst
                       end).
  + now intros [| x rst].
  + now intros [[] | [x rst]].
Qed.

list is a morphism

This means that if α == β, then list α == list β. We prove this by defining an instance of the Proper type class.

Instance list_Proper
  : Proper (type_equiv ==> type_equiv) list.

Proof.
  add_morphism_tactic.
  intros α β [f g equαβ equβα].
  equiv with (map f) and (map g).
  all: setoid_rewrite map_map; intros l.
  + replace (fun x : α => g (f x))
       with (@id α).
    ++ symmetry; apply map_id.
    ++ apply functional_extensionality.
       apply equαβ.
  + replace (fun x : β => f (g x))
       with (@id β).
    ++ symmetry; apply map_id.
    ++ apply functional_extensionality.
       apply equβα.
Qed.

The use of the Proper type class allows for leveraging hypotheses of the form α == β with the rewrite tactic. I personally consider providing instances of Proper whenever it is possible to be a good practice, and would encourage any Coq programmers to do so.

nat is a special purpose list

Did you notice? Now, using type_equiv, we can prove it!

Lemma nat_and_list : nat == list unit.
Proof.
  equiv with (fix to_list n :=
                match n with
                | S m => tt :: to_list m
                | _ => []
                end)
         and (fix of_list l :=
                match l with
                | _ :: rst => S (of_list rst)
                | _ => 0
                end).
  + induction x; auto.
  + induction y; auto.
    rewrite <- IHy.
    now destruct a.
Qed.

Non-empty lists

We can introduce a variant of list which contains at least one element by modifying the nil constructor so that it takes one argument instead of none.

Inductive non_empty_list (α : Type) :=
| ne_cons : α -> non_empty_list α -> non_empty_list α
| ne_singleton : α -> non_empty_list α.

We can demonstrate the relation between list and non_empty_list, which reveals an alternative implementation of non_empty_list. More precisely, we can prove that forall (α : Type), non_empty_list α == α * list α. It is a bit more cumbersome, but not that much. We first define the conversion functions, then prove they satisfy the properties expected by type_equiv.

Fixpoint non_empty_list_of_list {α} (x : α) (l : list α)
  : non_empty_list α :=
  match l with
  | y :: rst => ne_cons x (non_empty_list_of_list y rst)
  | [] => ne_singleton x
  end.

#[local]
Fixpoint list_of_non_empty_list {α} (l : non_empty_list α)
  : list α :=
  match l with
  | ne_cons x rst => x :: list_of_non_empty_list rst
  | ne_singleton x => [x]
  end.

Definition list_of_non_empty_list {α} (l : non_empty_list α)
  : α * list α :=
  match l with
  | ne_singleton x => (x, [])
  | ne_cons x rst => (x, list_of_non_empty_list rst)
  end.

Lemma ne_list_list_equiv (α : Type)
  : non_empty_list α == α * list α.

Proof.
  equiv with list_of_non_empty_list
         and (prod_curry non_empty_list_of_list).
  + intros [x rst|x]; auto.
    cbn.
    revert x.
    induction rst; intros x; auto.
    cbn; now rewrite IHrst.
  + intros [x rst].
    cbn.
    destruct rst; auto.
    change (non_empty_list_of_list x (α0 :: rst))
      with (ne_cons x (non_empty_list_of_list α0 rst)).
    replace0 :: rst)
      with (list_of_non_empty_list
              (non_empty_list_of_list α0 rst)); auto.
    revert α0.
    induction rst; intros y; [ reflexivity | cbn ].
    now rewrite IHrst.
Qed.

The sum Operator

sum Is a Morphism

To prove this, we compose together the functions whose existence is implied by α=α\alpha = \alpha' and β=β\beta = \beta'. To that end, we introduce the auxiliary function lr_map.

Definition lr_map_sum {α β α' β'} (f : α -> α') (g : β -> β')
    (x : α + β)
  : α' + β' :=
  match x with
  | inl x => inl (f x)
  | inr y => inr (g y)
  end.

Then, we prove sum is a morphism by defining a Proper instance.

Instance sum_Proper
  : Proper (type_equiv ==> type_equiv ==> type_equiv) sum.
Proof.
  add_morphism_tactic.
  intros α α' [fα gα' equαα' equα'α]
         β β' [fβ gβ' equββ' equβ'β].
  equiv with (lr_map_sum fα fβ)
         and (lr_map_sum gα' gβ').
  + intros [x|y]; cbn.
    ++ now rewrite <- equαα'.
    ++ now rewrite <- equββ'.
  + intros [x|y]; cbn.
    ++ now rewrite <- equα'α.
    ++ now rewrite <- equβ'β.
Qed.

sum Is Commutative

Definition sum_invert {α β} (x : α + β) : β + α :=
  match x with
  | inl x => inr x
  | inr x => inl x
  end.

Lemma sum_com {α β} : α + β == β + α.
Proof.
  equiv with sum_invert and sum_invert;
    now intros [x|x].
Qed.

sum Is Associative

The associativity of sum is straightforward to prove, and should not pose a particular challenge to prospective readersIf we assume that this article is well written, that is. .

Lemma sum_assoc {α β γ} : α + β + γ == α + (β + γ).
Proof.
  equiv with (fun x =>
                match x with
                | inl (inl x) => inl x
                | inl (inr x) => inr (inl x)
                | inr x => inr (inr x)
                end)
         and (fun x =>
                match x with
                | inl x => inl (inl x)
                | inr (inl x) => inl (inr x)
                | inr (inr x) => inr x
                end).
  + now intros [[x|x]|x].
  + now intros [x|[x|x]].
Qed.

sum Has A Neutral Element

We need to find a type ee such that α+e=α\alpha + e = \alpha for any type α\alpha (similarly to x+0=xx + 0 = x for any natural number xx).

Any empty type (that is, a type with no term such as False) can act as the natural element of Type. As a reminder, empty types in Coq are defined with the following syntaxNote that Inductive empty. is erroneous. When the := is omitted, Coq defines an inductive type with one constructor, making such a type equivalent to unit, not False. :

Inductive empty := .

From a high-level perspective, empty being the neutral element of sum makes sense. Because we cannot construct a term of type empty, then α + empty contains exactly the same numbers of terms as α. This is the intuition. Now, how can we convince Coq that our intuition is correct? Just like before, by providing two functions of types:

The first function is inl, that is one of the constructors of sum.

The second function is trickier to write in Coq, because it comes down to writing a function of type is empty -> α.

Definition from_empty {α} : empty -> α :=
  fun x => match x with end.

It is the exact same trick that allows Coq to encode proofs by contradiction.

If we combine from_empty with the generic function

Definition unwrap_left_or {α β}
    (f : β -> α) (x : α + β)
  : α :=
  match x with
  | inl x => x
  | inr x => f x
  end.

Then, we have everything to prove that α == α + empty.

Lemma sum_neutral (α : Type) : α == α + empty.
Proof.
  equiv with inl and (unwrap_left_or from_empty);
    auto.
  now intros [x|x].
Qed.

The prod Operator

This is very similar to what we have just proven for sum, so expect less text for this section.

prod Is A Morphism

Definition lr_map_prod {α α' β β'}
    (f : α -> α') (g : β -> β')
  : α * β -> α' * β' :=
  fun x => match x with (x, y) => (f x, g y) end.

Instance prod_Proper
  : Proper (type_equiv ==> type_equiv ==> type_equiv) prod.

Proof.
  add_morphism_tactic.
  intros α α' [fα gα' equαα' equα'α]
         β β' [fβ gβ' equββ' equβ'β].
  equiv with (lr_map_prod fα fβ)
         and (lr_map_prod gα' gβ').
  + intros [x y]; cbn.
    rewrite <- equαα'.
    now rewrite <- equββ'.
  + intros [x y]; cbn.
    rewrite <- equα'α.
    now rewrite <- equβ'β.
Qed.

prod Is Commutative

Definition prod_invert {α β} (x : α * β) : β * α :=
  (snd x, fst x).

Lemma prod_com {α β} : α * β == β * α.

Proof.
  equiv with prod_invert and prod_invert;
    now intros [x y].
Qed.

prod Is Associative

Lemma prod_assoc {α β γ}
  : α * β * γ == α * (β * γ).

Proof.
  equiv with (fun x =>
                match x with
                | ((x, y), z) => (x, (y, z))
                end)
         and (fun x =>
                match x with
                | (x, (y, z)) => ((x, y), z)
                end).
  + now intros [[x y] z].
  + now intros [x [y z]].
Qed.

prod Has A Neutral Element

Lemma prod_neutral (α : Type) : α * unit == α.

Proof.
  equiv with fst and ((flip pair) tt).
  + now intros [x []].
  + now intros.
Qed.

prod Has An Absorbing Element *)

And this absorbing element is empty, just like the absorbing element of the multiplication of natural numbers is 00 (that is, the neutral element of the addition).

Lemma prod_absord (α : Type) : α * empty == empty.
Proof.
  equiv with (snd >>> from_empty)
         and (from_empty).
  + intros [_ []].
  + intros [].
Qed.

prod And sum Distributivity

Finally, we can prove the distributivity property of prod and sum using a similar approach to prove the associativity of prod and sum.

Lemma prod_sum_distr (α β γ : Type)
  : α * (β + γ) == α * β + α * γ.
Proof.
  equiv with (fun x => match x with
                       | (x, inr y) => inr (x, y)
                       | (x, inl y) => inl (x, y)
                       end)
         and (fun x => match x with
                       | inr (x, y) => (x, inr y)
                       | inl (x, y) => (x, inl y)
                       end).
  + now intros [x [y | y]].
  + now intros [[x y] | [x y]].
Qed.

Bonus: Algebraic Datatypes and Metaprogramming

Algebraic datatypes are very suitable for generating functions, as demonstrated by the automatic deriving of typeclass in Haskell or trait in Rust. Because a datatype can be expressed in terms of sum and prod, you just have to know how to deal with these two constructions to start metaprogramming.

We can take the example of the fold functions. A fold function is a function which takes a container as its argument, and iterates over the values of that container in order to compute a result.

We introduce fold_type INPUT CANON_FORM OUTPUT, a tactic to compute the type of the fold function of the type INPUT, whose “canonical form” (in terms of prod and sum) is CANON_FORM and whose result type is OUTPUT. Interested readers have to be familiar with Ltac.

Ltac fold_args b a r :=
  lazymatch a with
  | unit =>
    exact r
  | b =>
    exact (r -> r)
  | (?c + ?d)%type =>
    exact (ltac:(fold_args b c r) * ltac:(fold_args b d r))%type
  | (b * ?c)%type =>
    exact (r -> ltac:(fold_args b c r))
  | (?c * ?d)%type =>
    exact (c -> ltac:(fold_args b d r))
  | ?a =>
    exact (a -> r)
  end.

Ltac currying a :=
  match a with
  | ?a * ?b -> ?c => exact (a -> ltac:(currying (b -> c)))
  | ?a => exact a
  end.

Ltac fold_type b a r :=
  exact (ltac:(currying (ltac:(fold_args b a r) -> b -> r))).

We use it to compute the type of a fold function for list.

Definition fold_list_type (α β : Type) : Type :=
  ltac:(fold_type (list α) (unit + α * list α)%type β).
fold_list_type =
  fun α β : Type => β -> (α -> β -> β) -> list α -> β
     : Type -> Type -> Type

It is exactly what you could have expected (as match the type of fold_right).

Generating the body of the function is possible in theory, but probably not in Ltac without modifying a bit type_equiv. This could be a nice use case for MetaCoq  though.