Zulip Chat Archive

Stream: new members

Topic: Code review: group structure


Isak Colboubrani (Feb 23 2024 at 22:16):

For educational purposes I'm building a group structure from scratch with the goal of a.) writing it in fully idiomatic Lean 4 code while b.) still understanding all the code I've written (not just copy and pasting).

Currently I have this code:

-- A magma is a set equipped with a binary operation.
class Magma (α : Type) extends Mul α

-- A semigroup is a magma whose binary operation is associative.
class Semigroup (α : Type) extends Magma α where
  mul_assoc:  g h k : α, (g * h) * k = g * (h * k)

-- A monoid is a semigroup with an identity element for the binary operation.
class One (α : Type) where
  one : α
instance One.toOfNat1 {α} [One α] : OfNat α (nat_lit 1) where
  ofNat := One α.1
class Monoid (α : Type) extends Semigroup α, One α where
  one_mul :  g : α, 1 * g = g
  mul_one :  g : α, g * 1 = g

-- A group is a monoid with an inverse for every element.
class Inv (α : Type) where
  inv : α  α
postfix:max "⁻¹" => Inv.inv
class Group (α : Type) extends Monoid α, Inv α where
  mul_left_inv:  g : α, g⁻¹ * g = 1
  mul_right_inv :  g : α, g * g⁻¹ = 1

-- An abelian group is a group whose binary operation is commutative.
class AbelianGroup (α : Type) extends Group α where
  mul_comm:  g h : α, g * h = h * g

Questions:

1.

Is there anything I can improve in the above to make it more idiomatic?

(I understand that extending Mul to introduce the name Magma is a bit odd, but for pedagogical reasons I'd like to spell out every part of the structure from magma and up.)

2.

I'm not fully comfortable with the following code snippet:

instance One.toOfNat1 {α} [One α] : OfNat α (nat_lit 1) where
  ofNat := One α.1

It feels somewhat magic. How should I understand it?

Previously I used …

class HasIdentityElement (α : Type) where
  e : α
notation "e" => HasIdentityElement.e

… which feels much more self-explaining, but I guess I must use One.toOfNat1 to get 1 instead of e for the identity?

Riccardo Brasca (Feb 23 2024 at 22:25):

Why not introducing Magma from scratch? Copying the definition of Mul.

Isak Colboubrani (Feb 23 2024 at 22:27):

Like this?

-- A magma is a set equipped with a binary operation.
class Magma (α : Type) where
  mul: α  α  α
infixl:70 (priority := high) "*" => Magma.mul

Riccardo Brasca (Feb 23 2024 at 22:28):

Ah, Mul is in Prelude, so you cannot not import it, and now your notation might conflict with the built in *.

Riccardo Brasca (Feb 23 2024 at 22:29):

Indeed using an high priority might work, but your first solution was probably better.

Riccardo Brasca (Feb 23 2024 at 22:30):

But I would use a notation for Magma, instead of a new class.

Riccardo Brasca (Feb 23 2024 at 22:31):

With

class Magma (α : Type) extends Mul α

if h : Magma α, then h.1 : Mul α, and to get to the operation you have to write h.1.1 (this is rarely needed of course, but still).

Riccardo Brasca (Feb 23 2024 at 22:33):

Something like local notation "Magma" α => Mul α if you don't want to use Mul everywhere.

Riccardo Brasca (Feb 23 2024 at 22:33):

(you can also play the game of #mil chapter 7 and use a different symbol for the operation)

Riccardo Brasca (Feb 23 2024 at 22:36):

Concerning

instance One.toOfNat1 {α} [One α] : OfNat α (nat_lit 1) where
  ofNat := One α.1

you can just ignore it. The point is that Lean treats numerals in a special way (so that if you type 452353245324653456435 it understands it as a natural number, even if of course this one has never been defined explicitly). That code just set up a notation 1 : α for the element One.one : α.

Isak Colboubrani (Feb 23 2024 at 22:48):

Another question: in Lean code I often see notation defined as this: infixl:70 … " * " => ….

Is that equivalent to infixl:70 … "*" => … (note the lack of spaces)? Are the spaces ignored?

Eric Wieser (Feb 23 2024 at 22:49):

The spaces are used when printing

Isak Colboubrani (Feb 23 2024 at 22:57):

Which of these formulations is the idiomatic one?

class Group (α : Type) extends Monoid α, Inv α where 
class Group (α : Type u) extends Monoid α, Inv α where 
class Group (G : Type) extends Monoid G, Inv G where 
class Group (G : Type u) extends Monoid G, Inv G where 

Riccardo Brasca (Feb 23 2024 at 23:01):

Surely Type u is more general.

Riccardo Brasca (Feb 23 2024 at 23:03):

For the other choice it really depends, mathlib goes via crazy classes like docs#MulOneClass

Riccardo Brasca (Feb 23 2024 at 23:03):

anything should be fine at the beginning.

Kevin Buzzard (Feb 24 2024 at 06:12):

When I'm teaching I don't introduce universes at all. It's just one less thing to worry about. People are total universe fanatics here ("they're in the language so why not use them") but for teaching purposes I've never seen the point of them.

Kevin Buzzard (Feb 24 2024 at 06:14):

And I always use sensible notation not alpha. The whole alpha thing was just how mathlib was initially developed in 2017 and there's still plenty of it around but I found it obfuscated things in algebra files because you can't tell what assumptions there are on a type when it's called alpha, whereas in a group theory file you can start guessing that M is a monoid, G and H are groups, A is an abelian group etc

Isak Colboubrani (Feb 24 2024 at 12:40):

Thanks a lot for the helpful hints @Riccardo Brasca and @Kevin Buzzard! I'm now using Type instead of Type … throughout. I've also switched from the alpha thing to M, G, H, A, etc.

I've now reached the stage where I'm ready to define Ring.

I'm doing:

-- A ring is a set equipped with two binary operations, addition and
-- multiplication, where addition forms an abelian group, multiplication
-- forms a monoid, and multiplication is distributive over addition.
class Ring₁ (R : Type) extends AdditiveAbelianGroup R, Monoid R where
  left_distrib :  a b c : R, a * (b + c) = (a * b) + (a * c)
  right_distrib :  a b c : R, (a + b) * c = (a * c) + (b * c)

class Ring₂ (R : Type) extends AdditiveAbelianGroup R, Monoid R where
  left_distrib :  a b c : R, a * (b + c) = a * b + a * c
  right_distrib :  a b c : R, (a + b) * c = a * c + b * c

Ring₁ compiles whereas Ring₂ fails with failed to synthesize instance HAdd R R ?m.[…].

My questions:

  1. Does Ring₁ look correct?
  2. How can I make Ring₂ compile?

Full #mwe:

-- A magma is a set equipped with a binary operation.
class Magma (M : Type) where
  mul: M  M  M
infixl:70 (priority := high) " * " => Magma.mul
class AdditiveMagma (AM : Type) where
  add: AM  AM  AM
infixl:70 (priority := high) " + " => AdditiveMagma.add

-- A semigroup is a magma whose binary operation is associative.
class Semigroup (S : Type) extends Magma S where
  mul_assoc:  g h k : S, (g * h) * k = g * (h * k)
class AdditiveSemigroup (AS : Type) extends AdditiveMagma AS where
  add_assoc:  g h k : AS, (g + h) + k = g + (h + k)

-- A monoid is a semigroup with an identity element for the binary operation.
class One (O : Type) where
  one : O
instance One.toOfNat1 {O} [One O] : OfNat O (nat_lit 1) where
  ofNat := One O.1
class Monoid (M : Type) extends Semigroup M, One M where
  one_mul :  g : M, 1 * g = g
  mul_one :  g : M, g * 1 = g
class Zero (Z : Type) where
  zero : Z
instance Zero.toOfNat0 {Z} [Zero Z] : OfNat Z (nat_lit 0) where
  ofNat := Zero Z.1
class AdditiveMonoid (AM : Type) extends AdditiveSemigroup AM, Zero AM where
  one_add :  g : AM, 0 + g = g
  add_one :  g : AM, g + 0 = g

-- A group is a monoid with an inverse for every element.
class Inverse (I : Type) where
  inv : I  I
postfix:max "⁻¹" => Inverse.inv
class Group (G : Type) extends Monoid G, Inverse G where
  mul_left_inv:  g : G, g⁻¹ * g = 1
  mul_right_inv :  g : G, g * g⁻¹ = 1
class AdditiveInverse (AI : Type) where
  inv : AI  AI
prefix:max "-" => AdditiveInverse.inv
class AdditiveGroup (AG : Type) extends AdditiveMonoid AG, AdditiveInverse AG where
  add_left_inv:  g : AG, (-g) + g = 0
  add_right_inv :  g : AG, g + (-g) = 0

-- An abelian group is a group whose binary operation is commutative.
class AbelianGroup (A : Type) extends Group A where
  mul_comm:  g h : A, g * h = h * g
class AdditiveAbelianGroup (AA : Type) extends AdditiveGroup AA where
  add_comm:  g h : AA, g + h = h + g

-- A ring is a set equipped with two binary operations, addition and
-- multiplication, where addition forms an abelian group, multiplication
-- forms a monoid, and multiplication is distributive over addition.
class Ring₁ (R : Type) extends AdditiveAbelianGroup R, Monoid R where
  left_distrib :  a b c : R, a * (b + c) = (a * b) + (a * c)
  right_distrib :  a b c : R, (a + b) * c = (a * c) + (b * c)

class Ring₂ (R : Type) extends AdditiveAbelianGroup R, Monoid R where
  left_distrib :  a b c : R, a * (b + c) = a * b + a * c
  right_distrib :  a b c : R, (a + b) * c = a * c + b * c

Riccardo Brasca (Feb 24 2024 at 12:50):

Change infixl:70 (priority := high) " + " => AdditiveMagma.add to something like infixl:65 (priority := high) " + " => AdditiveMagma.add. Multiplication should have an higher priority than addition.

Riccardo Brasca (Feb 24 2024 at 12:51):

Otherwise the parenthesis are wrong in Ring₂ (the fact that Lean gives you an error rather than simply using the wrong parenthesis is a little subtler, I suggest you to ignore it for the moment).

Isak Colboubrani (Feb 24 2024 at 12:57):

Ah of course! Thanks!

After …

-infixl:70 (priority := high) " + " => AdditiveMagma.add
+infixl:65 (priority := high) " + " => AdditiveMagma.add

… I'm now doing …

class Ring (R : Type) extends AdditiveAbelianGroup R, Monoid R where
  left_distrib :  a b c : R, a * (b + c) = a * b + a * c
  right_distrib :  a b c : R, (a + b) * c = a * c + b * c

… which works!

Riccardo Brasca (Feb 24 2024 at 13:03):

If you want to see something slightly different try modules!

Riccardo Brasca (Feb 24 2024 at 13:04):

The basic class is docs#SMul

Isak Colboubrani (Feb 24 2024 at 14:02):

@Riccardo Brasca That's a good idea. Thanks! My current plan is to work myself up to Field and then tackle "two sets with operations": I'm thinking module, vector space and then inner product space.

I've proceeded with Field doing:

-- A commutative ring is a ring in which the multiplication operation is
-- commutative.
class CommutativeRing (CR : Type) extends Ring CR where
  mul_comm:  g h : CR, g * h = h * g

-- A field is a commutative ring which contains a multiplicative inverse
-- for every nonzero element.
class Field (F : Type) extends CommutativeRing F where
  mul_inv :  a : F, a  0   b : F, a * b = 1

Looks correct?

I'm not sure if my definition of Field, Zero and One currently implies 0 ≠ 1 (or if/when it should). I guess I could make (0 : F) ≠ 1 explicit in Field. Should I and would that be the appropriate way to handle that?

Riccardo Brasca (Feb 24 2024 at 14:04):

Following standard mathematical terminology, in a field 0 ≠ 1, and the trivial ring surely satisfies

 a : F, a  0   b : F, a * b = 1

(since there are no a ≠ 0), so you should change the definition. I mean, yours has a (small) mathematical problem.

Riccardo Brasca (Feb 24 2024 at 14:05):

What is your goal, to learn how to set things up?

Riccardo Brasca (Feb 24 2024 at 14:06):

Anyway, for groups you followed the approva via Inv, you can do the same with fields: it gives you the same advantages (i.e. the notation ⁻¹).

Kevin Buzzard (Feb 24 2024 at 14:38):

Note that for Inv it's convenient in type theory to allow Inv 0 to make sense, and in mathlib we define it to be 0. The problem with ∀ a : F, a ≠ 0 → ∃ b : F, a * b = 1 is the same as the problem you avoided by not writing ∀ g : G, ∃ h : G, g * h = 1 for group inverses. If you use this as definition of inverse then you realise that it's not a definition, you have to use the axiom of choice to get the h(g) you want for all g, and things are much more inconvenient. It's easier to define an Inv function on groups sending an element g to Inv g and then demanding g * Inv g = 1 (and also setting up notation g1g^{-1} for Inv g of course). The same is true for fields; don't just demand an inverse exists, ask for a function, and demand it has good properties, because in practice you want the function, not just some abstract existence statement. This involves defining Inv 0 and the easiest solution is to just let it be 0, because you can't make Lean "crash" by dividing by 0.

Isak Colboubrani (Feb 24 2024 at 17:33):

Riccardo Brasca said:

What is your goal, to learn how to set things up?

Good question! My goal is to learn how to formalize first-year undergraduate abstract algebra from scratch in Lean 4 with an initial focus on group theory.

Patrick Massot (Feb 24 2024 at 18:02):

Did you read Chapter 7 of #mil?

Isak Colboubrani (Feb 24 2024 at 23:11):

@Patrick Massot I've skimmed it and looked at the code before, but now I have read it. Thanks for writing it! It is a great learning resource. I like it a lot.

This is my attempt at extracting the group theory part of chapter 7.1 and then removing all dependencies on Mathlib and Prelude.lean to make it fully self-contained:

namespace GroupTheoryFromMILChapter7

class Mul₁ (α : Type) where
  mul : α  α  α
infixl:70 (priority := high) " * " => Mul₁.mul
class Semigroup₁ (α : Type) extends Mul₁ α where
  mul_assoc :  a b c : α, a * b * c = a * (b * c)
class One₁ (α : Type) where
  one : α
instance One₁.toOfNat1 {O} [One₁ O] : OfNat O (nat_lit 1) where
  ofNat := One₁ O.1
class MulOneClass₁ (α : Type) extends One₁ α, Mul₁ α where
  one_mul :  a : α, 1 * a = a
  mul_one :  a : α, a * 1 = a
class Monoid₁ (M : Type) extends Semigroup₁ M, MulOneClass₁ M
class Inv₁ (α : Type) where
  inv : α  α
postfix:max "⁻¹" => Inv₁.inv
class Group₁ (G : Type) extends Monoid₁ G, Inv₁ G where
  inv_mul :  a : G, a⁻¹ * a = 1
  mul_inv :  a : G, a * a⁻¹ = 1
class AbelianGroup₁ (A : Type) extends Group₁ A where
  mul_comm :  a b : A, a * b = b * a

Let me know if anything looks odd.

Isak Colboubrani (Feb 24 2024 at 23:51):

I noticed that my code still seems to work as expected after doing the following:

-instance One₁.toOfNat1 {O} [One₁ O] : OfNat O (nat_lit 1) where ofNat := One₁ O.1
+instance {O} [One₁ O] : OfNat O (nat_lit 1) where ofNat := One₁ O.1

When should I include the function name argument ("One₁.toOfNat1" in this case) and when is it okay to omit it?

Kevin Buzzard (Feb 25 2024 at 00:05):

If you omit it then it will be named automatically, and possibly poorly. You can see what it was named by typing whatsnew in before the instance.

Isak Colboubrani (Feb 25 2024 at 20:07):

@Riccardo Brasca @Kevin Buzzard Thanks for the hints regarding the field definition.

Here is my new try:

-- A commutative ring is a ring in which the multiplication operation is
-- commutative.
class CommutativeRing (R : Type) extends Ring R where
  mul_comm :  a b : R, a * b = b * a

-- A field is a commutative ring which contains a multiplicative inverse
-- for every nonzero element.
class Field (F : Type) extends CommutativeRing F, Inverse F where
  zero_ne_one : (0 : F)  1
  mul_inv_cancel :  a : F, a  0  a * a⁻¹ = 1
  inv_zero : (0 : F)⁻¹ = 0

Let me know if anything looks odd.

Kevin Buzzard (Feb 25 2024 at 21:43):

Looks good to me at first glance!


Last updated: May 02 2025 at 03:31 UTC