module Algebra.Group.Free.Words {ℓ} (A : Type ℓ) ⦃ _ : Discrete A ⦄ where
Free groups in terms of words🔗
While we have a generic construction of free groups as a higher inductive type, which has the correct universal property of the free group on any set, it’s sometimes more useful to have a “quotient-free” description of the elements of a free group — akin to what lists are to free monoids. There are a couple cases where this comes up:
- When showing that the free group on a discrete set is again discrete;
- When showing that the unit map is injective.
In both of these cases, while it would strictly speaking be possible to prove these directly from the universal property, this turns out to be very complicated. If we can instead implement free groups as plain old data, both become very easy!
The idea is the same as for monoids: we want to find a normal form for expressions in a free group, and then prove that these normal forms are closed under the group operations. Since a group is, in particular, a monoid, we might start with saying that a normal form will be a list, which handles both unit and associativity. But the elements of the free group on can’t simply be lists of since groups also have inverses. So we’ll define a letter to be an element of with the left summand standing for the inclusion of a generator and with the right summand being a negated generator and we’ll refer to a list of letters as a word.
private instance
_ : ∀ {n} → H-Level A (2 + n)
_ = basic-instance 2 (Discrete→is-set auto)
: Type ℓ
Letter Word = A ⊎ A
Letter
= List Letter Word
private
pattern pos x = inl x
pattern neg x = inr x
private variable
: A
a b c : Letter
x y z : Word xs ys zs
Once again we hit a snag, though: if there are any then every element of has multiple distinct representations at the level of words! For an extreme example, even the unit can be represented either as the empty word or as or as etc. We can fix this in one of two ways:
We could declare, by taking a quotient, that any word with arbitrary prefix and suffix will be identical to the word with the generator-inverse pair deleted.
While this works, we’re essentially right back where we started! we’d have to prove that (e.g.) our decision procedure for equality in respects deleting pairs.
We simply rule out these pathological words by requiring that the elements of the free group are reduced. If we arrange for “being reduced” to be a proposition, then we retain decidable equality, since we’re free to not compare the proofs!
Defining in terms of reduced words does have one pretty major downside, though: concatenation of words does not preserve reducedness, so we (nominally) have to insert a “normalisation” step that cancels out any new pairs. This requires being able to identify when adjacent elements are opposites — so must have decidable equality.
Reduced words🔗
While we could define “reduced” as “does not contain an adjacent (or pair”, working with negative data is generally very annoying. So we’ll instead seek an equivalent, positive definition of reducedness. First, we’ll need a helper function that inverts a letter, so we can have a single case that excludes both the and pairs.
: Letter → Letter
opp (pos x) = neg x
opp (neg x) = pos x opp
: ∀ x → opp (opp x) ≡ x
opp-invol (pos x) = refl
opp-invol (neg x) = refl
opp-invol
module opp = Equiv (Iso→Equiv (opp , iso opp opp-invol opp-invol))
Then, we define reducedness. Both the empty word and the word containing a single letter are reduced, since there’s nowhere for a pair to hide.
data Reduced : Word → Type ℓ where
: Reduced []
nil : ∀ x → Reduced (x ∷ []) one
The first nontrivial case, then, is when we have two elements and at the front of a word Not only must and not be opposites, but should also not be adjacent to an hiding in — so we ask that the longer be reduced, and not
: ∀ x (a≠ob : x ≠ opp y) (bxs : Reduced (y ∷ xs)) → Reduced (x ∷ y ∷ xs) cons
: Reduced (x ∷ xs) → Reduced xs
uncons-reduced (one _) = nil
uncons-reduced (cons _ a≠ob x) = x uncons-reduced
We can prove that this is a proposition by a simple pattern match; and, since we can decide at any point in a word whether two adjacent elements are opposites, we can also decide whether a word is reduced.
: is-prop (Reduced xs)
reduced-is-prop = refl
reduced-is-prop nil nil (one a) (one .a) = refl
reduced-is-prop (cons a p xs) (cons .a q ys) =
reduced-is-prop (cons a) prop! (reduced-is-prop xs ys)
ap₂
: ∀ xs → Dec (Reduced xs)
dec-reduced = yes nil
dec-reduced [] (x ∷ []) = yes (one x)
dec-reduced (x ∷ y ∷ xs) with x ≡? opp y | dec-reduced (y ∷ xs)
dec-reduced ... | yes p | _ = no λ where
(cons _ ¬p _) → ¬p p
... | no ¬p | yes a = yes (cons x ¬p a)
... | no ¬p | no ¬a = no λ a → ¬a (uncons-reduced a)
instance
_ : ∀ {n} → H-Level (Reduced xs) (suc n)
_ = prop-instance reduced-is-prop
_ : ∀ {xs} → Dec (Reduced xs)
_ = dec-reduced _
Implementing the group operation🔗
As discussed before, the group operation in can not simply be concatenation of words — we should cancel out an inner pair for example, when concatenating the words and to get We could implement a normalisation procedure, and define the group operation to be the normal form of — but this would traverse the second word even though it does not need to! The only place where a cancellable pair can arise when concatenating reduced words is at the boundary.
Instead, we’ll show that every letter induces an automorphism on the set of reduced words, a sort of “reducing cons”, which automatically cancels an opposing element at the head of the word if necessary. By iterating this procedure, we will obtain the group operation on
: Letter → Word → Word
act = x ∷ []
act x [] (y ∷ xs) with x ≡? opp y
act x ... | yes _ = xs
... | no _ = x ∷ y ∷ xs
We can demonstrate the behaviour of this function with the following helper lemma, saying that acts on to give back A similar case bash shows that acts on reduced words, too.
: act (opp x) (x ∷ xs) ≡ xs
act-opp-head {x = x} with opp x ≡? opp x
act-opp-head ... | no ¬r = absurd (¬r refl)
... | yes _ = refl
: ∀ x → Reduced xs → Reduced (act x xs)
act-is-reduced = one x
act-is-reduced x nil (one a) with x ≡? opp a
act-is-reduced x ... | yes p = nil
... | no ¬p = cons x ¬p (one a)
(cons a p ys) with x ≡? opp a
act-is-reduced x ... | yes p = ys
... | no ¬p = cons x ¬p (cons a p ys)
: Reduced (x ∷ xs) → act x xs ≡ x ∷ xs
act-reduced (one _) = refl
act-reduced (cons {y} x a≠ob p) with x ≡? opp y
act-reduced ... | yes a=ob = absurd (a≠ob a=ob)
... | no _ = refl
The final ingredient we’ll need about the act
ion of each letter is that it’s
invertible on the left, by the action of
This can again be shown by a case bash.
: Reduced xs → act (opp x) (act x xs) ≡ xs
act-opp = act-opp-head
act-opp nil {x = x} (one y) with x ≡? opp y
act-opp ... | yes a = ap (_∷ []) (opp.adjunctr a)
... | no ¬a = act-opp-head
{x = x} (cons {y} x' a≠ob xs) with x ≡? opp x'
act-opp {x = x} (cons {y} x' a≠ob xs) | yes a=ob with opp x ≡? opp y
act-opp ... | yes p = absurd (a≠ob (opp.adjunctl (sym a=ob) ∙ p))
... | no ¬p = ap (λ e → e ∷ y ∷ _) (opp.adjunctr a=ob)
{x = x} (cons {y} x' a≠ob xs) | no ¬p with opp x ≡? opp x
act-opp ... | yes _ = refl
... | no ¬r = absurd (¬r refl)
We define the group operation by letting each letter of the left word act on the right word, in turn. By induction on the left word, we can extend the lemma above to say that reduced words are closed under multiplication.
: Word → Word → Word
mul = xs
mul [] xs (x ∷ xs) ys = act x (mul xs ys)
mul
: ∀ xs → Reduced ys → Reduced (mul xs ys)
mul-reduced = x
mul-reduced [] x (x ∷ xs) ys = act-is-reduced x (mul-reduced xs ys) mul-reduced
The simple recursive definition of mul
is also compatible with concatenation
of words, in the sense that a concatenation
acts on a word
as though you had first let
act on
and then applied
on the result.
: ∀ xs → mul (xs ++ ys) zs ≡ mul xs (mul ys zs)
mul-++ = refl
mul-++ [] (x ∷ xs) = ap (act x) (mul-++ xs) mul-++
Using this comparison with composition of automorphisms, we can show
that, as long as the list
is reduced, the group operation mul
is associative. Since it is definitionally unital on the left, all that
remains to construct of the group structure are the inverses.
: ∀ xs → Reduced zs → mul (mul xs ys) zs ≡ mul xs (mul ys zs)
mul-assoc = refl
mul-assoc [] red (x ∷ xs) red = comm x (mul xs _) red ∙ ap (act x) (mul-assoc xs red) where
mul-assoc : ∀ x xs → Reduced ys → mul (act x xs) ys ≡ act x (mul xs ys)
comm = refl
comm x [] red (y ∷ xs) red with x ≡? opp y
comm x ... | yes a = sym (ap (λ e → act e (act y (mul xs _))) a ∙ act-opp (mul-reduced xs red))
... | no _ = refl
Inverses and nonreducedness🔗
As one might expect, the inverse of a word is computed by inverting each letter, and then reversing the entire thing. It’s actually pretty straightforward, if not a bit tedious, to prove that this is a correct definition of inverse, at least when the input word is reduced.
: Word → Word
inv = []
inv [] (x ∷ xs) = inv xs ∷r opp x
inv
: Reduced xs → mul (inv xs) xs ≡ []
act-inv = refl
act-inv nil (one x) = act-opp-head
act-inv (cons {x} {xs} y a≠ob α) =
act-inv (inv xs ++ opp x ∷ []) ++ opp y ∷ [] ⌝ (y ∷ x ∷ xs) ≡⟨ ap! (++-assoc (inv xs) _ _) ⟩
mul ⌜ (inv xs ++ opp x ∷ opp y ∷ []) (y ∷ x ∷ xs) ≡⟨ mul-++ (inv xs) ⟩
mul (inv xs) (act (opp x) ⌜ act (opp y) (y ∷ x ∷ xs) ⌝) ≡⟨ ap! (act-opp-head {y}) ⟩
mul (inv xs) ⌜ act (opp x) (x ∷ xs) ⌝ ≡⟨ ap! act-opp-head ⟩
mul (inv xs) xs ≡⟨ act-inv (uncons-reduced α) ⟩
mul [] ∎
The trouble comes when proving that inverting a word results in
another reduced word — inv
is
appending elements on the right, but we can only build reduced words by
appending on the left. To bridge this gap, we’ll actually take a
surprisingly classical detour: instead of proving that inv
preserves reducedness, we’ll prove
the contrapositive — it reflects nonreducedness.
Again, we’ll need an inductive definition saying that a word is not reduced. We can build a nonreduced word by appending an opposite pair at the front, or by finding a nonreduced pair in the tail.
data Nonreduced : Word → Type ℓ where
: ∀ ys t {t'} (t=ot : t' ≡ opp t) → Nonreduced (t' ∷ t ∷ ys)
here : ∀ {x} (nrxs : Nonreduced xs) → Nonreduced (x ∷ xs) there
It’s straightforward to show that if a word is not reduced, then it
is Nonreduced
, since we can decide
at each point whether we have an inverse pair, and finding no
such pairs would contradict the assumption of reducedness.
: ¬ Reduced xs → Nonreduced xs
to-nonreduced {[]} ¬red = absurd (¬red nil)
to-nonreduced {x ∷ []} ¬red = absurd (¬red (one x))
to-nonreduced {x ∷ y ∷ xs} ¬red =
to-nonreduced let
: x ≠ opp y → Nonreduced (x ∷ y ∷ xs)
work = there (to-nonreduced {y ∷ xs} (λ red → ¬red (cons x x≠y red)))
work x≠y in Dec-rec (here xs y) work auto
The converse, that nonreducedness implies not being reduced, is
simple: an element of Nonreduced
serves as an index, letting us contradict a specific constructor of
Reduced
.
: Nonreduced xs → ¬ Reduced xs
from-nonreduced (here ys t t=ot) (cons _ a≠ob red) = a≠ob t=ot
from-nonreduced (there nre) (cons _ a≠ob red) = from-nonreduced nre red from-nonreduced
The force of this definition comes from the following lemma, an
inversion principle for Nonreduced
with concatenation on the
right. Let’s walk through the type: it says that, if we have some prefix
and the index of an inverse pair in the larger word
we can either find an inverse pair in
— i.e.,
is inverse to the last element in
— or in
i.e. it it is inverse to the head of
split-nonreduced: ∀ xs → Nonreduced (xs ++ x ∷ ys)
→ Nonreduced (xs ∷r x) ⊎ Nonreduced (x ∷ ys)
The proof is actually just a case bash.
= inr x
split-nonreduced [] x (y ∷ []) (here _ _ x) = inl (here [] _ x)
split-nonreduced (y ∷ []) (there x) = inr x
split-nonreduced (x ∷ y ∷ xs) (here _ _ p) = inl (here _ _ p)
split-nonreduced (x ∷ y ∷ xs) (there nre) with split-nonreduced (y ∷ xs) nre
split-nonreduced ... | inl p = inl (there p)
... | inr p = inr p
We can then prove that, if there is an inverse pair in the inverse of a word we can find an inverse pair in the original list. The proof is a straightforward induction, first (trivially) ruling out the case where the list has a single element.
: ∀ xs → Nonreduced (inv xs) → Nonreduced xs
inv-nonreduced (x ∷ []) (there ()) inv-nonreduced
If we have a pair
in the inverse
we then have two cases to consider, by the inversion principle above.
Either the word
is nonreduced, in which case we’ve found what we’re looking for (the
here'
case), or the inverse of
is nonreduced, in which case (there'
) we proceed by induction.
(x ∷ y ∷ xs) p =
inv-nonreduced let
: Nonreduced (opp y ∷ opp x ∷ []) → Nonreduced (x ∷ y ∷ xs)
here' = λ where
here' (there (there ()))
(here _ _ p) → here xs y (sym (opp.adjunctr (opp.injective p)))
: Nonreduced (inv (y ∷ xs)) → Nonreduced (x ∷ y ∷ xs)
there' = there (inv-nonreduced (y ∷ xs) p)
there' p
: Nonreduced (inv xs ∷r opp y) ⊎ Nonreduced (opp y ∷ opp x ∷ [])
p' = split-nonreduced (inv xs) (subst Nonreduced (++-assoc (inv xs) _ _) p)
p' in [ there' , here' ] p'
By the contrapositive, inv
preserves reducedness, so we can put together a group structure.
: Reduced xs → Reduced (inv xs)
inv-reduced = contrapose λ ¬ixsr → from-nonreduced
inv-reduced (inv-nonreduced _ (to-nonreduced ¬ixsr))
private
module m = make-group
: make-group (Σ[ l ∈ Word ] Reduced l)
mkg .m.group-is-set = hlevel 2
mkg
.m.unit = [] , nil
mkg .m.mul (x , p) (y , q) = mul x y , mul-reduced x q
mkg .m.inv (x , p) = inv x , inv-reduced p
mkg
.m.assoc (x , _) (y , _) (z , r) = Σ-prop-path! (sym (mul-assoc x r))
mkg .m.invl (_ , x) = Σ-prop-path! (act-inv x)
mkg .m.idl _ = refl mkg
: Group ℓ
Free-group = to-group mkg
Free-group
open Free-object
The universal property🔗
We’re almost done with the case bashes, but now we have to provide a universal property for the group we’ve constructed. Fortunately, we have a convenient API for free objects which tells us what we have to define. First, we have the group itself, and we send a generator to the word consisting of exactly
: Free-object Grp↪Sets (el! A)
make-free-group .free = Free-group
make-free-group .unit = λ x → pos x ∷ [] , one _ make-free-group
Next, if we have a group and a function from the set of generators, we must extend this to a group homomorphism We’ll also do this by recursion on the words, sending letters to automorphisms of First, we can send a letter to the map and similarly to By recursion, we extend this to mapping words, sending the empty word to the unit.
.fold {G} f = total-hom (λ (xs , _) → go xs) pres module fold where
make-free-group module G = Group-on (G .snd)
open G using (_⋆_ ; _⁻¹) public
: Letter → ⌞ G ⌟ → ⌞ G ⌟
goL (pos x) g = f x ⋆ g
goL (neg x) g = f x ⁻¹ G.⋆ g
goL
: Word → ⌞ G ⌟
go = G.unit
go [] (x ∷ xs) = goL x (go xs) go
Then, we must prove that this preserves the group operation. This turns out to be a handful of case bashes, appealing to the group laws in so we trust that the reader can follow the proofs below.
: ∀ x y g → x ≡ᵢ opp y → goL x (goL y g) ≡ g
go-opp (pos x) (neg .x) g reflᵢ = G.cancell G.inverser
go-opp (neg x) (pos .x) g reflᵢ = G.cancell G.inversel
go-opp
: ∀ x g h → goL x (g ⋆ h) ≡ goL x g ⋆ h
go-ass (pos x) g h = G.associative
go-ass (neg x) g h = G.associative
go-ass
: ∀ x xs → go (act x xs) ≡ goL x (go xs)
go-letter = refl
go-letter x [] (y ∷ xs) with x ≡? opp y
go-letter x ... | yes p = sym (go-opp x y _ (Id≃path.from p))
... | no ¬p = refl
: ∀ xs ys → go (mul xs ys) ≡ go xs ⋆ go ys
go-mul = sym G.idl
go-mul [] ys (x ∷ xs) ys =
go-mul (act x (mul xs ys)) ≡⟨ go-letter x (mul xs _) ⟩
go (go (mul xs ys)) ≡⟨ ap (goL x) (go-mul xs ys) ⟩
goL x (go xs ⋆ go ys) ≡⟨ go-ass x (go xs) (go ys) ⟩
goL x (goL x (go xs) ⋆ go ys) ∎
: is-group-hom _ _ (λ (xs , _) → go xs)
pres .is-group-hom.pres-⋆ (x , p) (y , q) = go-mul x y pres
We must then prove that folding the singleton word with a function gives the result — we almost got it definitionally, but the computation above produces the identical instead.
.commute {_ , Y} = ext λ a → Group-on.idr Y make-free-group
Finally, we must prove that our fold is the unique group homomorphism with this property. This turns out to be — you guessed it — a ton more case bashes. First, we’ll show that a group homomorphism which sends the singleton list to agrees with our fold on letters.
.unique {Y} {f} g h = ext uniq where
make-free-group open fold {Y} f
module g = is-group-hom (g .preserves)
: ∀ x → g .hom (x ∷ [] , one x) ≡ goL x G.unit
g-one (pos x) =
g-one .hom (pos x ∷ [] , one (pos x)) ≡⟨ happly h x ⟩
g .intror refl ⟩
f x ≡⟨ G.unit ∎
f x ⋆ G(neg x) =
g-one .hom (neg x ∷ [] , one (neg x)) ≡⟨ g.pres-inv {pos x ∷ [] , one _} ⟩
g .hom (pos x ∷ [] , one (pos x)) ⁻¹ ≡⟨ ap G.inverse (happly h x) ⟩
g .intror refl ⟩
f x ⁻¹ ≡⟨ G.unit ∎ f x ⁻¹ ⋆ G
For positive generators, this is almost by definition, minding our extra unit on the right as above; for the negative case, we use that a group homomorphism commutes with the inverse. Then, we can extend this to show that takes a word to the result of letting act on by our fold above. This is by cases on the evidence that the word is reduced.
: ∀ x xs α → g .hom (x ∷ xs , α) ≡ goL x (g .hom (xs , uncons-reduced α))
g-cons (pos x) [] (one .(pos x)) =
g-cons .hom (pos x ∷ [] , one (pos x)) ≡⟨ happly h x ⟩
g .intror g.pres-id ⟩
f x ≡⟨ G.hom ([] , nil) ∎
f x ⋆ g (neg x) [] (one .(neg x)) =
g-cons .hom (neg x ∷ [] , one (neg x)) ≡⟨ g.pres-inv {pos x ∷ [] , one _} ⟩
g .hom (pos x ∷ [] , one (pos x)) ⁻¹ ≡⟨ ap G.inverse (happly h x) ⟩
g .intror g.pres-id ⟩
f x ⁻¹ ≡⟨ G.hom ([] , nil) ∎
f x ⁻¹ ⋆ g (y ∷ xs) α@(cons _ _ β) =
g-cons x .hom (x ∷ y ∷ xs , α) ≡⟨ ap₂ (λ e α → g .hom (e , α)) (sym (act-reduced α)) prop! ⟩
g .hom (act x (y ∷ xs) , _) ≡⟨ g.pres-⋆ (x ∷ [] , one _) (y ∷ xs , β) ⟩
g .hom (x ∷ [] , _) ⋆ g .hom (y ∷ xs , β) ≡⟨ ap₂ G._⋆_ (g-one x) (g-cons y xs β) ⟩
g .unit ⋆ goL y (g .hom (xs , _)) ≡˘⟨ go-ass x G.unit (goL y (g .hom _)) ⟩
goL x G(G.unit ⋆ goL y (g .hom (xs , _))) ≡⟨ ap (goL x) (G.idl ∙ sym (g-cons y xs β)) ⟩
goL x (g .hom (y ∷ xs , β)) ∎ goL x
Finally, we can put this together for an arbitrary reduced word, using the lemmas above for each case.
: ∀ xs (α : Reduced xs) → g .hom (xs , α) ≡ go xs
uniq = g.pres-id
uniq xs nil (one a) =
uniq xs .hom (a ∷ [] , one a) ≡⟨ g-cons _ _ (one a) ⟩
g (g .hom ([] , nil)) ≡⟨ ap (goL a) g.pres-id ⟩
goL a .unit ∎
goL a G(a ∷ b ∷ xs) (cons a x α) =
uniq .hom (a ∷ b ∷ xs , cons a x α) ≡⟨ g-cons _ _ (cons a x α) ⟩
g (g .hom (b ∷ xs , α)) ≡⟨ ap (goL a) (uniq (b ∷ xs) α) ⟩
goL a (goL b (go xs)) ∎ goL a