A setoid is a type equipped with an equivalence relation. Setoids come up when you need types with a better behaved equality relation, or when you want the equality relation to carry additional information. After completing this tutorial you will:
setoid
package.constructing types with an equality relation that’s better behaved than Idris’s built-in Equal
type.
types with an equality relation that carries additional information
If you want to see the source-code behind this tutorial, check the source-code out.
A relation over a type ty
in Idris is any two-argument type-valued function:
namespace Control.Relation
Rel : Type -> Type
Rel ty = ty -> ty -> Type
This definition and its associated interfaces ship with idris’s standard library. Given a relation rel : Rel ty
and x,y : ty
, we can form x `rel` y : Type
: the type of ways in which x
and y
can be related.
For example, two lists overlap when they have a common element: record Overlap {0 a : Type} (xs,ys : List a) where
Lists can overlap in exactly one position:
constructor Overlapping
common : a
lhsPos : common Elem
xs
rhsPos : common Elem
ys
Ex1 : Overlap [1,2,3] [6,7,2,8]
But they can overlap in several ways:
Ex1 = Overlapping
{ common = 2
, lhsPos = There Here
, rhsPos = There (There Here)
}
Ex2a ,
We can think of a relation
Ex2b ,
Ex2c : Overlap [1,2,3] [2,3,2]
Ex2a = Overlapping
{ common = 3
, lhsPos = There (There Here)
, rhsPos = There Here
}
Ex2b = Overlapping
{ common = 2
, lhsPos = There Here
, rhsPos = Here
}
Ex2c = Overlapping
{ common = 2
, lhsPos = There Here
, rhsPos = There (There Here)
}
rel : Rel ty
as the type of edges in a directed graph between vertices in ty
:
edges have a direction: the type rel x y
is different to rel y x
multiple different edges between the same vertices e1, e2 : rel x y
self-loops between the same vertex are allowed loop : rel x x
.
An equivalence relation is a relation that’s:
reflexive: we guarantee a specific way in which every element is related to itself;
symmetric: we can reverse an edge between two edges; and
transitive: we can compose paths of related elements into a single edge.
record Equivalence (A : Type) where
constructor MkEquivalence
0 relation: Rel A
reflexive : (x : A) -> relation x x
symmetric : (x, y : A) -> relation x y -> relation y x
transitive: (x, y, z : A) -> relation x y -> relation y z
-> relation x z
We equip the built-in relation Equal
with the structure of an equivalence relation, using the constructor Refl
and the stdlib functions sym
, and trans
: EqualityEquivalence : Equivalence a
EqualityEquivalence = MkEquivalence
{ relation = (===)
, reflexive = </span>x => Refl
, symmetric = </span>x,y,x_eq_y => sym x_eq_y
, transitive = </span>x,y,z,x_eq_y,y_eq_z => trans x_eq_y y_eq_z
}
We’ll use the following relation on pairs of natural numbers as a running example. We can represent an integer as the difference between a pair of natural numbers: infix 8 .-.
The
record INT where
constructor (.-.)
pos, neg : Nat
record SameDiff (x, y : INT) where
constructor Check
same : (x.pos + y.neg === y.pos + x.neg)
SameDiff x y
relation is equivalent to mathematical equation that states that the difference between the positive and negative parts is identical:
xpos − xneg = ypos − yneg
But, unlike the last equation which requires us to define integers and subtraction, its equivalent (.same)
is expressed using only addition, and so addition on Nat
is enough.
The relation SameDiff
is an equivalence relation. The proofs are straightforward, and a good opportunity to practice Idris’s equational reasoning combinators from Syntax.PreorderReasoning
: SameDiffEquivalence : Equivalence INT
This equational proof represents the single-step equational proof:
SameDiffEquivalence = MkEquivalence
{ relation = SameDiff
, reflexive = </span>x => Check $ <span class="IdrisFunction">Calc</span> $
|~ x.pos + x.neg
~~ x.pos + x.neg …(Refl)
"Calculate:
The mnemonic behind the ASCII-art is that the first step in the proof starts with a logical-judgement symbol ⊢, each step continues with an equality sign =, and justified by a thought bubble (...)
.
, symmetric = </span>x,y,x_eq_y => Check $ <span class="IdrisFunction">Calc</span> $
In this proof, we were given the proof
|~ y.pos + x.neg
~~ x.pos + y.neg ..<(x_eq_y.same)
x_eq_y.same : x.pos + y.neg = y.pos + x.neg
and so we appealed to the symmetric equation. The mnemonic here is that the last bubble in the thought bubble (...)
is replace with a left-pointing arrow, reversing the reasoning step. , transitive = </span>x,y,z,x_eq_y,y_eq_z => Check $ <span class="IdrisFunction">plusRightCancel</span> <span class="IdrisKeyword">\_</span> <span class="IdrisKeyword">\_</span> <span class="IdrisBound">y</span><span class="IdrisFunction">.pos</span><br /> $ Calc $<br /> <span class="IdrisData">|~</span> <span class="IdrisBound">x</span><span class="IdrisFunction">.pos</span> <span class="IdrisFunction">+</span> <span class="IdrisBound">z</span><span class="IdrisFunction">.neg</span> <span class="IdrisFunction">+</span> <span class="IdrisBound">y</span><span class="IdrisFunction">.pos</span><br /> <span class="IdrisData">~~</span> <span class="IdrisBound">x</span><span class="IdrisFunction">.pos</span> <span class="IdrisFunction">+</span> <span class="IdrisKeyword">(</span><span class="IdrisBound">y</span><span class="IdrisFunction">.pos</span> <span class="IdrisFunction">+</span> <span class="IdrisBound">z</span><span class="IdrisFunction">.neg</span><span class="IdrisKeyword">)</span> <span class="IdrisData">...</span><span class="IdrisKeyword">(</span><span class="IdrisFunction">solve</span> <span class="IdrisData">3</span> <span class="IdrisFunction">Monoid.Commutative.Free.Free</span><br /> <span class="IdrisKeyword">{</span><span class="IdrisBound">a</span> <span class="IdrisKeyword">=</span> <span class="IdrisFunction">Nat.Additive</span><span class="IdrisKeyword">}</span> $
This proof is a lot more involved:
(X 0 .+. X 1) .+. X 2
=-= X 0 .+. (X 2 .+. X 1))
~~ x.pos + (z.pos + y.neg) …(cong (x.pos +) $ <span class="IdrisBound">y\_eq\_z</span><span class="IdrisFunction">.same</span><span class="IdrisKeyword">)</span><br /> <span class="IdrisData">~~</span> <span class="IdrisKeyword">(</span><span class="IdrisBound">x</span><span class="IdrisFunction">.pos</span> <span class="IdrisFunction">+</span> <span class="IdrisBound">y</span><span class="IdrisFunction">.neg</span><span class="IdrisKeyword">)</span> <span class="IdrisFunction">+</span> <span class="IdrisBound">z</span><span class="IdrisFunction">.pos</span> <span class="IdrisData">...</span><span class="IdrisKeyword">(</span><span class="IdrisFunction">solve</span> <span class="IdrisData">3</span> <span class="IdrisFunction">Monoid.Commutative.Free.Free</span><br /> <span class="IdrisKeyword">{</span><span class="IdrisBound">a</span> <span class="IdrisKeyword">=</span> <span class="IdrisFunction">Nat.Additive</span><span class="IdrisKeyword">}</span> $
X 0 .+. (X 1 .+. X 2)
=-= (X 0 .+. X 2) .+. X 1)
~~ (y.pos + x.neg) + z.pos …(cong (+ z.pos) ?h2)
~~ z.pos + x.neg + y.pos …(solve 3 Monoid.Commutative.Free.Free
{a = Nat.Additive} $
(X 0 .+. X 1) .+. X 2
=-= (X 2 .+. X 1) .+. X 0)
}
y
into contact with the appropriate part of z
and x
to transform the term.Here we use the idris library Frex
that can perform such routine rearrangements for common algebraic structures. In this case, we use the commutative monoid simplifier from Frex
. If you want to read more about Frex
, check the paper out.
Idris’s Control.Relation
defines interfaces for properties like reflexivity and transitivity. While the setoid package doesn’t use them, we’ll use them in a few examples.
The Overlap
relation from Examples 1 and 2 is symmetric: Symmetric (List a) Overlap where
However,
symmetric xs_overlaps_ys = Overlapping
{ common = xs_overlaps_ys.common
, lhsPos = xs_overlaps_ys.rhsPos
, rhsPos = xs_overlaps_ys.lhsPos
}
Overlap
is neither reflexive nor transitive:
The empty list doesn’t overlap with itself: Ex3 : Not (Overlap [] [])
Ex3 nil_overlaps_nil = case nil_overlaps_nil.lhsPos of
_ impossible
Two lists may overlap with a middle list, but on different elements. For example: Ex4 : ( Overlap [1] [1,2]
The outer lists agree on
, Overlap [1,2] [2]
, Not (Overlap [1] [2]))
Ex4 =
( Overlapping 1 Here Here
, Overlapping 2 (There Here) Here
, <span class=“IdrisData”> one_overlaps_two => case one_overlaps_two.lhsPos of
There _ impossible
)
1
and 2
, respectively, but they can’t overlap on on the first element of either, which exhausts all possibilities of overlap.