Zulip Chat Archive
Stream: new members
Topic: Classes vs Structures
Yasmine Sharoda (Feb 01 2021 at 15:43):
Hello,
I am interested to know the reason why in Mathlib monoid
is defined as a class
, while monoid_hom
is defined as a structure
. What do we miss if we define both as classes or structures?
Also when projecting the member op
of a class monoid
, one need to qualify it as:
@monoid_class.op a m x1 x2
while projecting from a structure monoid
can be sugared as
m.op_struc x1 x2
Kevin Buzzard (Feb 01 2021 at 15:44):
Mathematically, if you have a type M
then probably you only want to consider one monoid structure on it. But if you have two monoids M and N, you might want to consider many monoid homomorphisms between them.
Kevin Buzzard (Feb 01 2021 at 15:45):
Class instances are automatically filled in by the type class inference system, which works under a "there is at most one term of this type" assumption.
Kevin Buzzard (Feb 01 2021 at 15:45):
so monoid_hom
definitely shouldn't be a class, but there is no harm in making monoid
a class.
Eric Wieser (Feb 01 2021 at 15:52):
Note that docs#is_monoid_hom is the class version of monoid_hom
, but it was considered a failed design because simp
was not able to use it
Yasmine Sharoda (Feb 01 2021 at 17:08):
Kevin Buzzard said:
Mathematically, if you have a type
M
then probably you only want to consider one monoid structure on it. But if you have two monoids M and N, you might want to consider many monoid homomorphisms between them.
But one might want to define on nat
a monoid(nat,+,0,..)
and monoid(nat,*,1,..)
Eric Wieser (Feb 01 2021 at 17:10):
In practice, mathlib does that by add_monoid(nat,+,0,..)
, which obviously doesn't scale to other monoid structures
Adam Topaz (Feb 01 2021 at 17:10):
This is more of an issue about morphisms and the distinction between (+) and (*).
Kyle Miller (Feb 01 2021 at 18:05):
A feature that class
provides is that, for example, every group is automatically a monoid, so you can apply theorems about monoids to groups without any fuss. There isn't quite so deep of a hierarchy for homomorphisms, so this kind of automatic casting seems like it would be less beneficial.
What appears to be a plausible design heuristic for classes vs structures is whether you plan to do rewrites of terms. "Bundling" a term with its properties (like with monoid_hom
) lets you rewrite that whole object while maintaining its properties, but if it were a class you'd have to worry about whether the associated typeclass still applies to the rewritten term. I think it's a lot less frequent rewriting an expression involving the monoid's type itself in comparison to rewriting expressions of homomorphisms. (If you work with finite sets, you can feel some of this with finset
vs a set
with a fintype
instance.)
If I understand correctly, Coq uses structures for its algebraic hierarchy, but they use a feature called unification hints to do the automatic casting.
Kevin Buzzard (Feb 01 2021 at 20:06):
@Yasmine Sharoda it's what looks like an extraordinary design decision that Lean's monoids are bound to the notation *
and we have a different but isomorphic class add_monoid
bound to the notation +
. I've never seen this approach in any other theorem prover. A lot of people are surprised by this! But we have a to_additive
machine which duplicates all the lemmas for us automatically (so only one of the two needs to be stated, the multiplicative one) and it works just fine! So we have two classes, monoid
and add_monoid
, and we can have precisely one of each structure on a type with no trouble.
Yasmine Sharoda (Feb 01 2021 at 20:59):
Kyle Miller said:
A feature that
class
provides is that, for example, every group is automatically a monoid, so you can apply theorems about monoids to groups without any fuss. There isn't quite so deep of a hierarchy for homomorphisms, so this kind of automatic casting seems like it would be less beneficial.What appears to be a plausible design heuristic for classes vs structures is whether you plan to do rewrites of terms. "Bundling" a term with its properties (like with
monoid_hom
) lets you rewrite that whole object while maintaining its properties, but if it were a class you'd have to worry about whether the associated typeclass still applies to the rewritten term. I think it's a lot less frequent rewriting an expression involving the monoid's type itself in comparison to rewriting expressions of homomorphisms. (If you work with finite sets, you can feel some of this withfinset
vs aset
with afintype
instance.)If I understand correctly, Coq uses structures for its algebraic hierarchy, but they use a feature called unification hints to do the automatic casting.
@Kyle Miller Can you please elaborate more on what you mean by "rewrites of terms"? Do you mean the terms of monoid
language? and what does that have to do with bundling?
Yasmine Sharoda (Feb 01 2021 at 21:06):
Kevin Buzzard said:
Yasmine Sharoda it's what looks like an extraordinary design decision that Lean's monoids are bound to the notation
*
and we have a different but isomorphic classadd_monoid
bound to the notation+
. I've never seen this approach in any other theorem prover. A lot of people are surprised by this! But we have ato_additive
machine which duplicates all the lemmas for us automatically (so only one of the two needs to be stated, the multiplicative one) and it works just fine! So we have two classes,monoid
andadd_monoid
, and we can have precisely one of each structure on a type with no trouble.
I understand that design decision. The automation provided by to_additive
and other meta programs is what distinguishes Mathlib in a really good way. What I am asking is more of having a class like this one
class monoid_class (m : Type) : Type :=
(e : m)
(op : m -> m -> m)
(assoc: ∀ x y z : m, op x (op y z) = op (op x y) z)
(lunit : ∀ x : m, op e x = x)
(runit : ∀ x : m, op x e = x)
and then creating 2 instances for nat, where you once instantiate op
to be +
and another time instantiate it to be *
.
I am not sure if the two scenarios are the same of different!
Kyle Miller (Feb 01 2021 at 21:46):
With the monoid and monoid homomorphism examples, by rewriting terms I mean rewriting expressions involving monoids themselves or of monoid homomorphisms. For example, maybe you know that M = f M'
and then rewrite according to this. Since you have [monoid M]
, you don't have [monoid (f M')]
without rewriting the instance, too, so f M'
isn't automatically a monoid itself.
Eric Wieser (Feb 01 2021 at 21:46):
Ultimately you have to hook into has_add
, and I don't know how you'd do that with that example
Kyle Miller (Feb 01 2021 at 21:46):
The issue with your monoid_class
is that you can't extend monoid_class
twice
Kyle Miller (Feb 01 2021 at 21:46):
and op
would be a name conflict
Kyle Miller (Feb 01 2021 at 21:48):
and also you're only supposed to have one instance of a class per set of indices, so you couldn't have nat
be both a +
monoid and a *
monoid in a natural way. You'd want op
to be one of the indices for the class
for that, but then assoc
and the rest would be name conflicts when you extend. If lean had some way to rename fields when extending, it might work.
Yasmine Sharoda (Feb 02 2021 at 01:21):
Kyle Miller said:
With the monoid and monoid homomorphism examples, by rewriting terms I mean rewriting expressions involving monoids themselves or of monoid homomorphisms. For example, maybe you know that
M = f M'
and then rewrite according to this. Since you have[monoid M]
, you don't have[monoid (f M')]
without rewriting the instance, too, sof M'
isn't automatically a monoid itself.
Thanks for the clarification.
Yasmine Sharoda (Feb 02 2021 at 01:34):
The second part of my question is why projecting a field of class monoid
requires different syntax from projecting one from structure monoid
.
- In the case of a
class
, one has to use@monoid_class.op a m x1 x2
- In the case of a
structure
, one can usem.op x1 x2
(wherem
is the name of themonoid
instance)
Is that just overlooking syntactic sugar? or is there a more profound reason?
Kyle Miller (Feb 02 2021 at 04:30):
If monoid_class
is a structure, the type of op
is
monoid_class.op : Π {m : Type}, monoid_class m → m → m → m
As a class, the type is
monoid_class.op : Π {m : Type} [c : monoid_class m], m → m → m
The difference between these is that class instance variable is automatically set to being inferred by the typeclass resolution system. This gives you the ability to write this:
open monoid_class (op)
example {m : Type*} [monoid_class m] (x y z : m) :
op x (op y z) = op (op x y) z :=
monoid_class.assoc x y z
(The open
line imports monoid_class.op
into the current namespace, so you can write op
for short.)
Kyle Miller (Feb 02 2021 at 04:31):
What you are observing is that @monoid_class.op
changes the class instance argument to needing to be explicitly given. When all goes well, you don't need to pass class instance arguments.
Kyle Miller (Feb 02 2021 at 04:33):
When a class extends another, there is an automatically defined class instance definition. This lets you do the following:
class semigroup_class (m : Type) : Type :=
(op : m -> m -> m)
(assoc: ∀ x y z : m, op x (op y z) = op (op x y) z)
class monoid_class (m : Type) extends semigroup_class m : Type :=
(e : m)
(lunit : ∀ x : m, op e x = x)
(runit : ∀ x : m, op x e = x)
open semigroup_class (op)
example {m : Type*} [monoid_class m] (x y z : m) :
op x (op y z) = op (op x y) z :=
semigroup_class.assoc x y z
Yasmine Sharoda (Feb 02 2021 at 20:39):
Kyle Miller said:
What you are observing is that
@monoid_class.op
changes the class instance argument to needing to be explicitly given. When all goes well, you don't need to pass class instance arguments.
Thank you!
Last updated: Dec 20 2023 at 11:08 UTC