A common programming task is working with sets, which are collections of objects without any particular order. In practice, a simple way to handle small sets is to store the elements in a list (or array) and ensure no duplicates, while also making sure that functions on these lists do not rely on the order of the elements. However, typical programming languages do not enforce these invariants, and programmers must take extra care. One common trick is to keep the list sorted, which can reduce the likelihood of accidentally relying on element order. This approach, however, is limited, especially when working with elements that have no natural ordering, or when sorting is too costly.
In mathematics, one possible definition of a multiset is that it is a list of elements, but we impose an equivalence relation: two lists are equivalent if one is a permutation of the other. For example, [1, 2, 3] is equivalent to [3, 2, 1] but not to [1, 2, 2]. In this context, a multiset is the set of all lists that are permutations of each other—i.e., an equivalence class. In Lean, we can work with such equivalence classes directly using quotients.
Unordered Pair
Let’s start with a simpler example: unordered pairs of two natural numbers. Suppose we begin with an ordered pair (a, b) and define an equivalence relation such that (a, b) ~ (c, d) if either a = c ∧ b = d or a = d ∧ b = c.
Now we define an unordered pair as the quotient of ℕ × ℕ by this equivalence:
defUnorderedPair:=Quotequiv
The Quot type constructor in Lean takes an equivalence relation r : X → X → Prop and creates a new type that represents the equivalence classes of X under this relation. Essentially, Quot r is a way to work with sets of equivalent values without dealing directly with the specific elements of these sets.
When working with quotients in Lean, it’s useful to understand how Lean manages equivalence classes. Internally, Lean treats each quotient type as if it stores just one representative from each equivalence class. However, unlike in some other programming languages, Lean enforces that any functions defined on these quotient types must be proven to be independent of the choice of representative. This means you must ensure that your functions produce consistent results regardless of which specific element of the equivalence class is used.
Let’s define a function that sums the elements of an unordered pair. In Lean, defining functions on quotients requires using Quot.lift, which lets you work with representatives while ensuring that the result is independent of which representative is chosen:
⊢ ∀ (a b : ℕ × ℕ),
equivab →
(funx =>
matchxwith
| (a, b) => a + b)
a =
(funx =>
matchxwith
| (a, b) => a + b)
b
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
⊢ equiv (a, b) (c, d) →
(funx =>
matchxwith
| (a, b) => a + b)
(a, b) =
(funx =>
matchxwith
| (a, b) => a + b)
(c, d)
;
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
⊢ a = c ∧ b = d ∨ a = d ∧ b = c → a + b = c + d
;
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h
:
a = c ∧ b = d ∨ a = d ∧ b = c
⊢ a + b = c + d
;
inl
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h✝
:
a = c ∧ b = d
⊢ a + b = c + d
inr
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h✝
:
a = d ∧ b = c
⊢ a + b = c + d
caseinlh
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h
:
a = c ∧ b = d
⊢ a + b = c + d
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h
:
a = c ∧ b = d
⊢ c + b = c + d
;
All goals completed! 🐙
caseinrh
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h
:
a = d ∧ b = c
⊢ a + b = c + d
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h
:
a = d ∧ b = c
⊢ d + b = c + d
;
p
:
UnorderedPair
a
:
ℕ
b
:
ℕ
c
:
ℕ
d
:
ℕ
h
:
a = d ∧ b = c
⊢ d + c = c + d
;
All goals completed! 🐙
)p
Here, Quot.lift takes three arguments: a function that works on representatives, fun (a, b) => a + b, a proof that the function respects the equivalence relation and an element of the quotient type p.
To create an element of a quotient, we use Quot.mk. For example, to construct an unordered pair:
Let's explore a more intricate example involving symbolic expressions with free variables and a single binary operation. Our goal is to handle expressions in a way that respects mathematical properties such as associativity and the existence of a unit element.
We start by defining an inductive type Expr to represent these symbolic expressions:
mul x y, representing the multiplication of two expressions x and y.
We would like the multiplication to to be associative and one as multiplicative unit. Therefore we need to define equivalence relation on Expr that expresses this and then take an quotient of Expr.
One way of defining this equivalence relation is to defined normalization function that takes and expression and normalizes it to an unique element. In this particular case, we remove all multiplications by one and right associate all multiplication. We achieve this by mapping Expr to List Nat and back:
Instead of using Quot we will use Quotient which provides a better user experience. To use Quotient we have to provide an instance of Setoid Expr which attaches relation to Expr and proves that it is an equivalence:
trans⊢ ∀ {x y z : Expr}, x.normalize = y.normalize → y.normalize = z.normalize → x.normalize = z.normalize
trans
x✝
:
Expr
y✝
:
Expr
z✝
:
Expr
h
:
x✝.normalize = y✝.normalize
q
:
y✝.normalize = z✝.normalize
⊢ x✝.normalize = z✝.normalize
;
trans
x✝
:
Expr
y✝
:
Expr
z✝
:
Expr
h
:
x✝.normalize = y✝.normalize
q
:
y✝.normalize = z✝.normalize
⊢ y✝.normalize = z✝.normalize
;
All goals completed! 🐙
Now we can define the quotinet of Expr:
defExprMonoid:=QuotientExprSetoid
Based on the name, ExprMonoid forms mathematical monoid. To prove all the necessary monoid theorems we will need the that Expr.ofList is injective function:
To define multiplication we used Quotient.liftOn₂ insteald of Quot.lift. Also instead of Quot.mk to create an element of quotient we used the notation ⟦·⟧. The proofs are rather verbose on purpose so you can step through them step by step and get an idea what does it takes to prove properties when using quotients.
For debuging purposes we might want to print out an element of ExprMonoid. To maintain consistency of the system we have to print out the string representation of the normalized element:
Sometime we might also want to see the actual representant of (e : ExprMonoid). We can accesse it with Quotient.unquot but this function is unsafe as it might produce different results for propositionally equal arguments i.e. an unsafe function f might for arguments x and y that are equail, x = y, produce different results f x ≠ f y.
defvar(n:Nat):ExprMonoid:=⟦.varn⟧defe:=var0*var1*1*1*var0
-- show the mathematical element
"(#0 * (#1 * (#0 * 1)))"
#evale.toString
-- show the underlining representation
"((((#0 * #1) * 1) * 1) * #0)"
#evale.toStringRepr
-- show the underlining representation after normalization
"(#0 * (#1 * (#0 * 1)))"
#evale.normalize.toStringRepr
This example demonstrates converting and normalizing symbolic expressions into their string representations, showcasing the power of quotients in Lean for maintaining mathematical invariants while managing complex symbolic data.