Martin Escardo 23rd Feb 2026. A result by David Wärn [1].
Moved from `gist` to this place 17th April 2026.
Merged basic library into UF.Base on 3—4 June 2026 by Tom de Jong.
Any type equipped with a binary operation _*_ that is associative,
commutative and idempotent is a set (homotopy 0-type). That is, there
are no higher semilattices! This file is my attempt to understand
David's Agda file [1].
[1] David Wärn. https://dwarn.se/agda/Idem.html , 17 February 2026.
(See also https://mathstodon.xyz/deck/@dwarn/116091515645003634)
I add, in the discussion of the formal definitions and proofs, some
diagrams and some explanations, and rename some things, which I hope
are correct and what David intended. You may wish to also have a look
at yet another version [2].
[2] Tom de Jong. AlgebraicStructuresForcingSethood.Semilattices-streamlined.lagda,
25-27 February 2026.
(See also https://mathstodon.xyz/deck/@de_Jong_Tom/116141966476412629)
Proof sketch (formalized in the anonymous module at the very end of
this file). Fix a type A and an x₀ : A, and write ΩA = (x₀ = x₀). An
operation _*_ on A induces an operation _⋆_ on ΩA. The main steps
are, for suitably defined act-l and act-r,
(i) p ⋆ p = p (⋆-idemp)
(ii) p ⋆ q = act-l p ∙ act-r q (⋆-in-terms-of-∙)
(iii) p ⋆ q = q ⋆ p (comm-loop)
(iv) act-l p = act-r p (only-one-act)
(v) act-l p ∙ act-l p = p (act-l-idemp)
(vi) act-l p ∙ act-l q = act-l q ∙ act-l p (comm-l)
An Eckmann–Hilton argument using (v) and (vi) gives the following:
(vii) p ∙ q = q ∙ p (loop-comm)
Finally, using the associativity of *, we get that
(viii) act-l (act-l p) = act-l p (act-l-idem)
(ix) act-l p = refl (act-l-trivial)
(x) p = refl (Ω-null)
In the spirit of the original file [1], we only use minimal imports
from TypeTopology, to show that a relatively short argument in a
rather Spartan MLTT is possible - we only need Π-types and identity
types. (We also don't make real use of universe polymorphism.)
\begin{code}
{-# OPTIONS --safe --without-K #-}
module AlgebraicStructuresForcingSethood.Semilattices where
open import MLTT.Universes
open import MLTT.Id
open import UF.Base using
( ap₂
; refl-left-neutral
; refl-right-neutral
; cancel-left
; =-congr
; =-congr-refl
; =-congr-∙
; =-congr-∙'
; =-congr-nat
; =-congr-⁻¹
; =-congr-sq
; comm₂
)
\end{code}
We fix a type A with an associative, commutative, idempotent
operation, and a base-point x₀.
\begin{code}
module _
(A : 𝓤 ̇ )
(_*_ : A → A → A)
(idem : (a : A) → a * a = a)
(comm : (a b : A) → a * b = b * a)
(assoc : (a b c : A) → (a * b) * c = a * (b * c))
(x₀ : A)
where
ΩA : 𝓤 ̇
ΩA = x₀ = x₀
\end{code}
The operation * acts on paths componentwise:
a₁ ══ p ══ a₂ b₁ ══ q ══ b₂
↝ a₁*b₁ ══ p ★ q ══ a₂*b₂.
It also splits into a left part followed by a right part:
a₁*b₁ ══ ap (-*b₁) p ══ a₂*b₁ ══ ap (a₂*-) q ══ a₂*b₂.
\begin{code}
_★_ : {a₁ a₂ b₁ b₂ : A} → a₁ = a₂ → b₁ = b₂ → a₁ * b₁ = a₂ * b₂
_★_ = ap₂ _*_
★-in-terms-of-∙ : {a₁ a₂ b₁ b₂ : A} (p : a₁ = a₂) (q : b₁ = b₂)
→ p ★ q = ap (_* b₁) p ∙ ap (a₂ *_) q
★-in-terms-of-∙ refl refl = refl
\end{code}
The function `reduce` uses idempotence to turn a loop at x₀ * x₀ into
a loop at x₀:
idem x₀
x₀ ════════ x₀*x₀
║ ║
║ ║
║ ║
x₀ ════════ x₀*x₀
idem x₀
\begin{code}
reduce : x₀ * x₀ = x₀ * x₀ → ΩA
reduce = =-congr (idem x₀) (idem x₀)
\end{code}
Given loops p, q : x₀ = x₀, we form the loop at x₀*x₀ via _★_, then
reduce it:
x₀*x₀ ══════ p ★ q ══════ x₀*x₀
║ ║
idem x₀ idem x₀
║ ║
x₀ ═══════════════════════ x₀
p ⋆ q
It splits as act-l p ∙ act-r q:
x₀*x₀ ═ ap(-*x₀) p ═ x₀*x₀ ═ ap(x₀*-) q ═══ x₀*x₀
║ ║ ║
idem x₀ idem x₀ idem x₀
║ ║ ║
x₀ ═════ act-l p ════ x₀ ═════ act-r q ═════ x₀
With this we have that _★_ induces an operation _⋆_ on loops.
\begin{code}
_⋆_ : ΩA → ΩA → ΩA
p ⋆ q = reduce (p ★ q)
act-l : ΩA → ΩA
act-l p = reduce (ap (_* x₀) p)
act-r : ΩA → ΩA
act-r q = reduce (ap (x₀ *_) q)
⋆-in-terms-of-∙ : (p q : ΩA) → p ⋆ q = act-l p ∙ act-r q
⋆-in-terms-of-∙ p q =
ap reduce (★-in-terms-of-∙ _ _)
∙ =-congr-∙ (idem x₀) (idem x₀) (idem x₀) (ap (_* x₀) p) (ap (x₀ *_) q)
\end{code}
We now show that _⋆_ is idempotent.
When both arguments are equal, p ★ p reduces to p via the idempotence
of *, for any p : a = b:
a*a ══════ p ★ p ═════ b*b
║ ║
idem a idem b
║ ║
a ═════════ p ════════ b
\begin{code}
★-idemp : {a b : A} (p : a = b)
→ =-congr (idem a) (idem b) (p ★ p) = p
★-idemp refl = =-congr-refl (idem _)
⋆-idemp : (p : ΩA) → p ⋆ p = p
⋆-idemp = ★-idemp
\end{code}
Next, we show that _⋆_ is commutative.
We use the commutativity of * as a base-point-preserving loop
comm-self : x₀ = x₀, and show that equality congruence by it swaps _⋆_.
comm-loop-raw builds the following stacked rectangle:
x₀*x₀ ═══ p ★ q ════ x₀*x₀ (top, before reduce)
║ ║
comm x₀ x₀ comm x₀ x₀ (side paths via commutativity)
║ ║
x₀*x₀ ═══ q ★ p ════ x₀*x₀ (comm-paths flips the args)
║ ║
idem x₀ idem x₀
║ ║
x₀ ═════ q ⋆ p ══════ x₀
Stacking the rectangles gives
eq-congr comm-self comm-self (p ⋆ q) = q ⋆ p.
The function comm-self-center then shows that comm-self is central
(equality congruence with it is the identity), by specializing to p = q
and using ⋆-idemp.
\begin{code}
comm-paths : {a b x y : A} (p : a = x) (q : b = y)
→ =-congr (comm a b) (comm x y) (p ★ q) = q ★ p
comm-paths refl refl = =-congr-refl (comm _ _)
comm-self : ΩA
comm-self = reduce (comm x₀ x₀)
comm-loop-raw : (p q : ΩA)
→ =-congr comm-self comm-self (p ⋆ q) = q ⋆ p
comm-loop-raw p q =
=-congr
(=-congr-nat (comm x₀ x₀) (comm x₀ x₀) (idem x₀) (idem x₀) (p ★ q))
refl
(ap reduce (comm-paths p q))
comm-self-center : (p : ΩA) → =-congr comm-self comm-self p = p
comm-self-center p =
=-congr
(ap (=-congr comm-self comm-self) (⋆-idemp p))
(⋆-idemp p)
(comm-loop-raw p p)
comm-loop : (p q : ΩA) → p ⋆ q = q ⋆ p
comm-loop p q = =-congr (comm-self-center _) refl (comm-loop-raw p q)
\end{code}
The function `act-swap` defined below follows from comm-loop via the
splitting ⋆-in-terms-of-∙:
act-l p ∙ act-r q
= p ⋆ q (by ⋆-in-terms-of-∙)
= q ⋆ p (by comm-loop)
= act-l q ∙ act-r p
To deduce act-l = act-r, we specialize to q = refl:
act-l p ∙ act-r refl = act-l refl ∙ act-r p
act-l p ∙ refl = refl ∙ act-r p
act-l p = act-r p
\begin{code}
act-swap : (p q : ΩA) → act-l p ∙ act-r q = act-l q ∙ act-r p
act-swap p q = =-congr
(⋆-in-terms-of-∙ p q)
(⋆-in-terms-of-∙ q p)
(comm-loop p q)
act-l-refl : act-l refl = refl
act-l-refl = =-congr-refl (idem x₀)
act-r-refl : act-r refl = refl
act-r-refl = =-congr-refl (idem x₀)
only-one-act : (p : ΩA) → act-l p = act-r p
only-one-act p = =-congr
(ap (act-l p ∙_) act-r-refl ∙ refl-right-neutral _)
(ap (_∙ act-r p) act-l-refl ∙ refl-left-neutral)
(act-swap p refl)
\end{code}
Using an Eckmann–Hilton argument, we show that loop concatenation is
commutative.
We now know
(v) act-l p ∙ act-l p = p (using act-l = act-r)
(vi) act-l p ∙ act-l q = act-l q ∙ act-l p (comm-l, using act-swap)
Then
p ∙ q
= (act-l p ∙ act-l p) ∙ (act-l q ∙ act-l q) (by act-l-idemp twice)
= (act-l q ∙ act-l q) ∙ (act-l p ∙ act-l p) (by comm₂, using comm-l)
= q ∙ p
\begin{code}
act-l-idemp : (p : ΩA) → act-l p ∙ act-l p = p
act-l-idemp p =
ap (act-l p ∙_) (only-one-act p)
∙ =-congr (⋆-in-terms-of-∙ p p) refl (⋆-idemp p)
comm-l : (p q : ΩA) → act-l p ∙ act-l q = act-l q ∙ act-l p
comm-l p q =
=-congr
(ap (act-l p ∙_) ((only-one-act q) ⁻¹))
(ap (act-l q ∙_) ((only-one-act p) ⁻¹))
(act-swap p q)
loop-comm : (p q : ΩA) → p ∙ q = q ∙ p
loop-comm p q =
=-congr
(ap₂ _∙_ (act-l-idemp p) (act-l-idemp q))
(ap₂ _∙_ (act-l-idemp q) (act-l-idemp p))
(comm₂ {p = act-l p} {q = act-l q} (comm-l p q))
\end{code}
We now show that _⋆_ is associative.
The function assoc-paths defined below says that * acts functorially
on 2×1 grids of paths:
(a*b)*c ══ (p ★ q) ★ r ══ (x*y)*z
║ ║
assoc a b c assoc x y z
║ ║
a*(b*c) ══ p ★ (q ★ r) ══ x*(y*z)
We compare two reductions of the triple self-product x₀*x₀*x₀, one
for each parenthesization:
(x₀*x₀)*x₀ ══ idem-triple-l ══ x₀
x₀*(x₀*x₀) ══ idem-triple-r ══ x₀
The function path assoc-self is the loop
x₀ ═ (by sym idem-triple-l)
(x₀*x₀)*x₀ ═ (by assoc)
x₀*(x₀*x₀) ═ (by idem-triple-r)
x₀
that witnesses the path between the two constructions. Equality
congruence with assoc-self gives ⋆-assoc-raw. Since ΩA is commutative
(loop-comm) and the equality congruence witness satisfies the square
identity (=-congr-sq), we can use ∙-cancel to strip assoc-self and
obtain ⋆-assoc.
\begin{code}
assoc-paths : {a b c x y z : A} (p : a = x) (q : b = y) (r : c = z)
→ =-congr (assoc a b c) (assoc x y z) ((p ★ q) ★ r)
= p ★ (q ★ r)
assoc-paths refl refl refl = =-congr-refl (assoc _ _ _)
idem-triple-l : (x₀ * x₀) * x₀ = x₀
idem-triple-l = (idem x₀ ★ refl) ∙ idem x₀
idem-triple-r : x₀ * (x₀ * x₀) = x₀
idem-triple-r = (refl ★ idem x₀) ∙ idem x₀
\end{code}
We now have, recorded as triple-fold-l,
(a*b)*d ════════════ (p ★ q) ★ r ══════════════════════ (a*b)*d
║ ║
(hab ★refl) ∙ hcd (hab ★ refl) ∙ hcd
║ ║
e ══ =-congr hcd hcd (=-congr hab hab (p ★ q) ★ r) ══ e
And we have that triple-fold-r does the same for the right-associated
parenthesization.
\begin{code}
triple-fold-l : {a b c d e : A} (hab : a * b = c) (hcd : c * d = e)
(p : a = a) (q : b = b) (r : d = d)
→ =-congr
((hab ★ refl) ∙ hcd)
((hab ★ refl) ∙ hcd)
((p ★ q) ★ r)
= =-congr
hcd
hcd
(=-congr hab hab (p ★ q) ★ r)
triple-fold-l refl refl p q r = refl
triple-fold-r : {a b c d e : A} (hab : a * b = c) (hcd : d * c = e)
(p : a = a) (q : b = b) (r : d = d)
→ =-congr
((refl ★ hab) ∙ hcd)
((refl ★ hab) ∙ hcd)
(r ★ (p ★ q))
= =-congr
hcd
hcd
(r ★ =-congr hab hab (p ★ q))
triple-fold-r refl refl p q r = refl
loop-triple-l : (p q r : ΩA)
→ =-congr idem-triple-l idem-triple-l ((p ★ q) ★ r)
= (p ⋆ q) ⋆ r
loop-triple-l p q r = triple-fold-l (idem x₀) (idem x₀) p q r
loop-triple-r : (p q r : ΩA)
→ =-congr idem-triple-r idem-triple-r (p ★ (q ★ r))
= p ⋆ (q ⋆ r)
loop-triple-r p q r = triple-fold-r (idem x₀) (idem x₀) q r p
assoc-self : ΩA
assoc-self = idem-triple-l ⁻¹ ∙ (assoc x₀ x₀ x₀ ∙ idem-triple-r)
⋆-assoc-raw : (p q r : ΩA)
→ =-congr assoc-self assoc-self ((p ⋆ q) ⋆ r)
= p ⋆ (q ⋆ r)
⋆-assoc-raw p q r =
=-congr
((=-congr-∙' (assoc x₀ x₀ x₀) idem-triple-r _ idem-triple-r _) ⁻¹
∙ (=-congr-∙' _ (assoc x₀ x₀ x₀ ∙ idem-triple-r) _ _ _) ⁻¹)
(loop-triple-r p q r)
(ap (=-congr idem-triple-r idem-triple-r)
(ap (=-congr (assoc x₀ x₀ x₀) _)
((=-congr-⁻¹ {hax = idem-triple-l} {hby = idem-triple-l}
(loop-triple-l p q r)) ⁻¹)
∙ assoc-paths p q r))
⋆-refl : (p : ΩA) → p ⋆ refl = act-l p
⋆-refl p =
⋆-in-terms-of-∙ p refl
∙ ap (act-l p ∙_) act-r-refl
∙ refl-right-neutral (act-l p)
\end{code}
We strip assoc-self using the commutativity of ΩA and the square
identity:
assoc-self ∙ ⋆-assoc-raw = (p ⋆ q) ⋆ r ∙ assoc-self
(by =-congr-sq once we know loop-comm).
\begin{code}
⋆-assoc : (p q r : ΩA) → (p ⋆ q) ⋆ r = p ⋆ (q ⋆ r)
⋆-assoc p q r =
cancel-left (loop-comm assoc-self _ ∙ =-congr-sq _ assoc-self _ ⁻¹)
∙ ⋆-assoc-raw p q r
\end{code}
We now conclude that every loop is trivial.
act-l is idempotent:
act-l (act-l p)
= act-l (p ⋆ refl) (by ⋆-refl)
= p ⋆ (refl ⋆ refl) (by ⋆-assoc)
= p ⋆ (act-l refl) (by ⋆-refl)
= p ⋆ refl (by act-l-refl)
= act-l p (by ⋆-refl)
\begin{code}
act-l-idem : (p : ΩA) → act-l (act-l p) = act-l p
act-l-idem p =
=-congr
(⋆-refl _ ∙ ap act-l (⋆-refl p))
(ap (p ⋆_) (⋆-refl refl ∙ act-l-refl) ∙ ⋆-refl p)
(⋆-assoc p refl refl)
\end{code}
act-l p = refl.
act-l p ∙ act-l p
= act-l (act-l p) ∙ act-l (act-l p) (act-l-idem twice)
= act-l p (act-l-idemp)
So act-l p acts as a right-identity on itself, and we can left-cancel
to get refl.
\begin{code}
act-l-trivial : (p : ΩA) → act-l p = refl
act-l-trivial p =
cancel-left
((ap₂ _∙_
((act-l-idem p) ⁻¹)
((act-l-idem p) ⁻¹)
∙ act-l-idemp (act-l p))
∙ refl-right-neutral (act-l p))
\end{code}
Every loop is trivial:
p = act-l p ∙ act-l p = refl ∙ refl = refl.
The first step is act-l-idemp backwards, and the second is
act-l-trivial. This shows that the type A is a set, as we wished to
know.
\begin{code}
Ω-null : (p : x₀ = x₀) → p = refl
Ω-null p =
(act-l-idemp p) ⁻¹
∙ ap₂ _∙_ (act-l-trivial p) (act-l-trivial p)
∙ refl-left-neutral
\end{code}