From 41007fce2a333ba78be703c35f4afccade3369e5 Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Sun, 12 Jul 2020 10:25:42 +0200 Subject: New article on Algebraic Datatypes --- site/posts/AlgebraicDatatypes.v | 678 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 678 insertions(+) create mode 100644 site/posts/AlgebraicDatatypes.v (limited to 'site/posts/AlgebraicDatatypes.v') diff --git a/site/posts/AlgebraicDatatypes.v b/site/posts/AlgebraicDatatypes.v new file mode 100644 index 0000000..b5a786f --- /dev/null +++ b/site/posts/AlgebraicDatatypes.v @@ -0,0 +1,678 @@ +(** * Proving Algebraic Datatypes are “Algebraic” *) + +(** 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: + +<< +enum List { + Cons(A, Box), + Nil, +} +>> + + In Coq: + +<< +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. + + - [a + b] is the disjoint union of types [a] and [b]. Any term of [a] + can be injected into [a + b], and the same goes for [b]. Conversely, + a term of [a + b] can be projected into either [a] or [b]. + - [a * b] is the Cartesian product of types [a] and [b]. Any term of [a * + b] is made of one term of [a] and one term of [b] (remember tuples?). + + 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 reminescent of a disjoint + union. Coming back to the [list] type, under the syntactic sugar of + algebraic datatypes, the [list α] type is equivalent to [unit + α * list α], + where [unit] models the [nil] case, and [α + list α] 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, + + - [+] is commutative, that is ##\forall (x, y),\ x + y + = y + x## + - [+] is associative, that is ##\forall (x, y, z),\ (x + + y) + z = x + (y + z)## + - [+] has a neutral element, that is ##\exists e_s, + \ \forall x,\ x + e_s = x## + - [*] is commutative, that is ##\forall (x, y),\ x * y + = y * x## + - [*] is associative, that is ##\forall (x, y, z),\ (x + * y) * z = x * (y * z)## + - [*] has a neutral element, that is ##\exists e_p, + \ \forall x,\ x * e_p = x## + - The distributivity of [+] and [*], that is ##\forall + (x, y, z),\ x * (y + z) = x * y + x * z## + - [*] has an absorbing element, that is ##\exists e_a, + \ \forall x, \ x * e_a = e_a## + + 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 +>> + + #
site/posts/AlgebraicDatatypes.v
# + + #
# *) + +From Coq Require Import + Basics Setoid Equivalence Morphisms + List FunctionalExtensionality. +Import ListNotations. + +Set Implicit Arguments. + +(** ** 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, [α + β] is not structurally _equal_ to [β + α], yet + we will have to prove they are “equivalent.” *) + +(** *** Introducing [type_equiv] *) + +(** Since [=] for [Type] is not suitable for reasoning about algebraic + datatypes. As a consequence, we first introduce our own equivalence + relation, denoted [==]. + + We say two types [α] and [β] are equivalent up to an isomorphism (denoted by + [α == β]) when for any term of type [α], there exists a counter-part term of + type [β] and vice versa. In other words, [α] and [β] are equivalent if we + can exhibit two functions [f] and [g] such that: + + ##\forall (x : α),\ x = g(f(x))## + + ##\forall (y : β),\ y = f(g(y))## + + In Coq, this translates into the following inductive types. *) + +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 <> 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 *) + +(** [type_equiv] is an equivalence, and we can prove it by demonstrating it is + (1) reflexive, (2) symmetric, and (3) transitive. + + [type_equiv] is reflexive. *) + +Lemma type_equiv_refl (α : Type) : α == α. + +(** This proof is straightforward. A type [α] is equivalent to itself because: + + ##\forall (x : α),\ x = id(id(x))## *) + +Proof. + now equiv with (@id α) and (@id α). +Qed. + +(** [type_equiv] is symmetric. *) + +Lemma type_equiv_sym {α β} (equ : α == β) : β == α. + +(** If [α == β], then we know there exists two functions [f] and [g] which + satisfy the expected properties. We can “swap” them to prove that [β == α]. + *) + +Proof. + destruct equ as [f g equ1 equ2]. + now equiv with g and f. +Qed. + +(** [type_equiv] is transitive *) + +Lemma type_equiv_trans {α β γ} (equ1 : α == β) (equ2 : β == γ) + : α == γ. + +(** If [α == β], we know there exists two functions [fα] and [gβ] which satisfy + the expected properties of [type_equiv]. Similarly, because [β == γ], we + know there exists two additional functions [fβ] and [gγ]. We can compose + these functions together to prove [α == γ]. + + As a reminder, composing two functions [f] and [g] (denoted by [f >>> g] + thereafter) consists in using the result of [f] as the input of [g]: *) + +Infix ">>>" := (fun f g x => g (f x)) (at level 70). + +(** Then comes the proof. *) + +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. + +(** 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 prod_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 prod_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)). + replace (α0 :: 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 *) + +(** This means that if [α == α'] and [β == β'], then [α + β == α' + β']. To + prove this, we compose together the functions whose existence is implied by + [α == α'] and [β == β']. 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 perspective readers; if 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 [e] such that [α + e == α] for any type [α] + (similarly to ##x~+~0~=~x## for any natural + number ##x## that is). + + 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 syntax: *) + +Inductive empty := . + +(** Note that the following definition is erroneous. + +<< +Inductive empty. +>> + + Using [Print] on such a type illustrates the issue. + +<< +Inductive empty : Prop := Build_empty { } +>> + + That is, when the [:=] is omitted, Coq defines an inductive type with one + constructor. + + Coming back to [empty] being the neutral element of [sum]. From a high-level + perspective, this 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: + + - [α -> α + empty] + - [α + empty -> α] + + The first function is [inl], that is one of the constructor of [sum]. + + The second function is more tricky 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 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 number is ##0##, + 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 proving 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 + demonstrating 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 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 <>, whose “canonical form” (in + terms of [prod] and [sum]) is <> and whose result type is + <>. 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 computing the type of a [fold] function for [list]. *) + +Definition fold_list_type (α β : Type) : Type := + ltac:(fold_type (list α) (unit + α * list α)%type β). + +(** Here is the definition of [fold_list_type], as outputed by [Print]. + +<< +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. *) -- cgit v1.2.3