langston-barrrett.github.io
A Quick Tutorial on Setoids in Coq
A setoid is a type T and an
equivalence
relation
(=) : T -> T -> Prop=. Coq has some built-in automation
facilities for working with setoids that can make your life a lot
easier.
Building a setoid
Let's suppose we wanted to implement a quick-and-dirty version of
finite sets on some type A. One option is to use
list A, but lists have two properties that get in the way:
ordering and duplication. Building a Setoid allows us to
abstract away these features, and consider two lists equivalent if they
have the same elements.
Context {A : Type}.
Definition equiv_list (l1 l2 : list A) : Prop :=
∀ a : A, In a l1 <-> In a l2.
To register our new equivalence with Coq and reap the benefits of
automation, we make list A an instance of the
Setoid
typeclass:
Instance ListSetoid : Setoid (list A) :=
{ equiv := equiv_list }.
Proof.
split.
At this point, we're dropped into proof mode and our goal is to prove
that equiv_list is an equivalence relation. To see the full
proof, see the Coq source for this
post
(we use some setoid-related automation to prove this goal).
Immediately, we gain access to a few tactics that work for normal
equality, like
reflexivity,
symmetry
(plus symmetry in [...]), and
transitivity.
setoid_rewrite
The #1 benefit to using setoids is access to the
setoid_rewrite tactic. Rewriting is one of the most
powerful features of the tactic language, and using setoids allows us to
expand its reach.
Suppose we have l1 l2 : list A. If we have
l1 = l2 (where = is Coq's built-in equality),
we can replace l1 with l2 in all contexts:
Lemma hd_eq (l1 l2 : list A) : l1 = l2 -> hd l1 = hd l2.
Proof.
intros l1eql2.
now rewrite l1eql2.
Qed.
This isn't true if we just know l1 = l2= (where
== is
notation
for list_equiv). However, there are certain contexts in
which they are interchangeable. Suppose further that A has
decidable equality, and consider the following remove
function:
Context {deceq : ∀ x y : A, {x = y} + {x ≠ y}}.
Fixpoint remove (x : A) (lst : list A) : list A :=
match lst with
| nil => nil
| cons y ys =>
match deceq x y with
| left _ => remove x ys
| right _ => y :: remove x ys
end
end.
Since this removes all occurrences of x in
lst, we should be able to prove
∀ x l1 l2, l1 == l2 -> remove x l1 = remove x l2
We state this lemma a bit oddly:
Instance removeProper : Proper (eq ==> equiv ==> equiv) remove.
This concisely states "given two equal inputs x y : A and
two equivalent lists l1 l2 : list A,
remove x l1 is equivalent to remove y l2". In
other words, remove respects equality on A
(trivially -- every Coq function respects equality) and the equivalence
relation == (AKA equiv) on
list A.
Once we have an Instance of Proper, we can use
setoid_rewrite to replace replace remove x l1
with remove x l2, even when they appear nested in the goal
statement:
Instance revProper : Proper (equiv ==> equiv) (@rev A).
Proof.
[...]
Qed.
Lemma remove_rev_remove {a b : A} {l1 l2 : list A} :
l1 == l2 -> rev (remove a l1) == rev (remove a l2).
Proof.
intros H.
now setoid_rewrite H.
Qed.
We can even compose Proper instances automatically:
Require Import Coq.Program.Basics.
Local Open Scope program_scope.
Instance rev_remove_Proper : ∀ a, Proper (equiv ==> equiv) (@rev A ∘ remove a).
Proof.
apply _.
Qed.
The benefits of setoid rewriting continue to increase with the complexity of the goal.