Open mjungmath opened 3 years ago
Digging into the source code, the forgetful functor F
calls the following code from its class parent Functor
:
def _apply_functor(self, x):
"""
Apply the functor to an object of ``self``'s domain.
NOTE:
Each subclass of :class:`Functor` should overload this method. By default,
this method coerces into the codomain, without checking whether the
argument belongs to the domain.
TESTS::
sage: from sage.categories.functor import Functor
sage: F = Functor(FiniteFields(),Fields())
sage: F._apply_functor(ZZ)
Rational Field
"""
return self.__codomain(x)
Here, __codomain
should be the destination category. However, the call function of a category is treated as some kind of coercion/conversion:
def __call__(self, x, *args, **opts):
"""
Construct an object in this category from the data in ``x``,
or throw ``TypeError`` or ``NotImplementedError``.
If ``x`` is readily in ``self`` it is returned unchanged.
Categories wishing to extend this minimal behavior should
implement :meth:`._call_`.
EXAMPLES::
sage: Rings()(ZZ)
Integer Ring
"""
if x in self:
return x
return self._call_(x, *args, **opts)
as the following note in sage/categories/sets_cat.py
indicates, too:
def _call_(self, X, enumerated_set=False):
r"""
Construct an object in this category from the data ``X``.
INPUT:
- ``X`` -- an object to be converted into a set
- ``enumerated_set`` -- if set to ``True`` and the input is either a
Python tuple or a Python list then the output will be a finite
enumerated set.
EXAMPLES::
sage: Sets()(ZZ)
Integer Ring
sage: Sets()([1, 2, 3])
{1, 2, 3}
sage: S = Sets()([1, 2, 3]); S.category()
Category of finite sets
sage: S = Sets()([1, 2, 3], enumerated_set=True); S.category()
Category of facade finite enumerated sets
.. NOTE::
Using ``Sets()(A)`` used to implement some sort of forgetful functor
into the ``Sets()`` category. This feature has been removed, because
it was not consistent with the semantic of :meth:`Category.__call__`.
Proper forgetful functors will eventually be implemented, with
another syntax.
"""
if enumerated_set and type(X) in (tuple, list, range):
from sage.categories.enumerated_sets import EnumeratedSets
return EnumeratedSets()(X)
from sage.sets.set import Set
return Set(X)
Proper forgetful functors will eventually be implemented, with another syntax. It seems, this hasn't been done so far.
One could try the following: overload _apply_functor
for forgetful functors and call _call_
directly. That would be a minimal solution, but I am not sure whether it is the most elegant.
I don't think this is a bug. The code is performing as it should because ZZ
is already in Sets()
. It doesn't need to create a new object, much less an instance of a different class. I think if you want to change your object, you should call the explicit constructor rather than relying on a functor. Moreover, if it is not going to be the current behavior, then I think you're asking for a solution to an impossible problem.
Replying to @tscrim:
I don't think this is a bug. The code is performing as it should because
ZZ
is already inSets()
. It doesn't need to create a new object, much less an instance of a different class.
Then, the same would hold for manifolds in #31241. Differentiable manifolds are also in the category of topological manifolds (or at least should be). But we already agreed that it's a bug there. What is the difference then?
I think if you want to change your object, you should call the explicit constructor rather than relying on a functor. Moreover, if it is not going to be the current behavior, then I think you're asking for a solution to an impossible problem.
Well, the forgetful functor has the purpose to forget imposed structures, and see the object as an object in the super category only. This condition is currently not met:
sage: from sage.categories.functor import ForgetfulFunctor
....: F = ForgetfulFunctor(Rings(), Sets())
....: F(ZZ).category()
Join of Category of euclidean domains and Category of infinite enumerated sets and Category of metric spaces
sage: F(ZZ)(1) + F(ZZ)(2)
3
Usual set operations are not even compatible:
sage: Set([1,2,3]).union(F(ZZ))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
...
TypeError: X (=Integer Ring) must be a Set
Of course, different structures need different implementations.
For manifolds, the current stage of behavior is rather bad. Differentiable manifolds can be seen as topological manifolds, however each differentiable manifold comes with a differentiable structure which Sage keeps implicitly track of. This must be dropped after the forgetful functor had been applied. But with the above behavior, this is not possible because the original instance would be returned.
Sage development has entered the release candidate phase for 9.3. Setting a new milestone for this ticket based on a cursory review of ticket status, priority, and last modification date.
Another example:
sage: R.<x> = QQ[]
sage: phi = R.hom([x^2]); phi
Ring endomorphism of Univariate Polynomial Ring in x over Rational Field
Defn: x |--> x^2
sage: phi.category_for()
Join of Category of euclidean domains and Category of commutative algebras over (number fields and quotient fields and metric spaces) and Category of infinite sets
sage: F = ForgetfulFunctor(Rings(), Sets())
sage: F(phi)
Ring endomorphism of Univariate Polynomial Ring in x over Rational Field
Defn: x |--> x^2
I hoped to compute the correct category of an image in #32121 (Replace MapCombinatorialClass
, add methods Map.image
, Map.pushforward
)...
This in and of itself is not a bug IMO as per comment:4. However, what is a bug IMO is that
sage: F(phi).parent()
Set of Homomorphisms from Univariate Polynomial Ring in x over Rational Field
to Univariate Polynomial Ring in x over Rational Field
which leads to this
sage: F(phi).category_for()
Join of Category of euclidean domains
and Category of commutative algebras over (number fields and quotient fields and metric spaces)
and Category of infinite sets
For morphisms, where the category is part of the homset constructor, we can create the new homset parent and then convert the morphism into that parent. Mostly this just consists of rebuilding the same object but with a different parent. This is meaningful when we want to compose maps (as opposed to manipulating sets).
Description changed:
---
+++
@@ -13,4 +13,10 @@
sage: Set(ZZ)
Set of elements of Integer Ring
+so that:
+- F(ZZ).category()
gives InfiniteEnumeratedSets()
+- ... and thus Hom(F(ZZ), ...)
will construct morphisms in the correct category
+- it uses equality as sets, not as rings
+ +
Description changed:
---
+++
@@ -19,4 +19,6 @@
- it uses equality as sets, not as rings
+Tickets:
+- #34384 Proper `ForgetfulFunctor` with `codomain=Sets()` or its full subcategories
Replying to @tscrim:
I don't think this is a bug. The code is performing as it should because
ZZ
is already inSets()
. It doesn't need to create a new object, much less an instance of a different class.
Mathematically one would, of course, say that the forgetful functor should act like the identity on a parent.
But our parents are equipped with a distinguished category.
So the forgetful functor needs to construct a new parent in which the distinguished category is replaced by the codomain.
Description changed:
---
+++
@@ -21,4 +21,4 @@
Tickets:
- #34384 Proper `ForgetfulFunctor` with `codomain=Sets()` or its full subcategories
-
+- #31241 Forgetful Functors for Manifold Objects
Replying to @mkoeppe:
Replying to @tscrim:
I don't think this is a bug. The code is performing as it should because
ZZ
is already inSets()
. It doesn't need to create a new object, much less an instance of a different class.Mathematically one would, of course, say that the forgetful functor should act like the identity on a parent.
That strongly depends on how mathematically precise you want to connect a parent to its mathematical model. Strictly speaking, ZZ
is just a set, but the ring of integers is really (ZZ, +, *)
with distinguished binary operations +: ZZ x ZZ -> ZZ
and *: ZZ x ZZ -> ZZ
. However, the "ring" ZZ
is just modeling the set and it is the implementation of the elements that carry the information of +
and *
. However, if you never add or multiply the elements together, then 1
is just an element of the set ZZ
.
But our parents are equipped with a distinguished category.
I think it is better to treat it like "this is the smallest category where this object (currently) makes sense to belong to."
So the forgetful functor needs to construct a new parent in which the distinguished category is replaced by the codomain.
I still contest that the set of objects is still the set of objects. Equality is not always the mathematical equality you are thinking of (which we actually often think of as being "isomorphic to").
Even allowing that we wanted a new parent, this would be an impossible problem to solve as we would either have to have a class for the forgetful functor for each category (a huge number) or we would need to know which methods to remove (any parent can implement their own custom version or ones that only make sense in category C'
not its super category C
).
If the user wants to do equality as some other type of object, then we should tell the user to build other type. For example, Set(ZZ)
to do equality as a Set
object.
Morphisms already have a hook for specifying the category. This might not be fully implemented, but if we wanted to consider the isomorphism x |-> 2x
of Q as a Q-vector space, but it doesn't make sense as a ring morphism. The forgetful functor should change the category of the morphism.
Replying to @tscrim:
Even allowing that we wanted a new parent, this would be an impossible problem to solve as we would either have to have a class for the forgetful functor for each category (a huge number) or we would need to know which methods to remove (any parent can implement their own custom version or ones that only make sense in category
C'
not its super categoryC
).
I agree that it is impossible to solve in general.
That should not stop us from implementing it for useful cases. In #34384, I implement it for codomain=Sets()
.
There's warnings for the rest. We could also raise a NotImplementedError
.
Replying to @tscrim:
Replying to @mkoeppe:
our parents are equipped with a distinguished category.
... namely, what the method category()
returns.
I think it is better to treat it like "this is the smallest category where this object (currently) makes sense to belong to."
... this is ill-defined or tautological
Replying to @tscrim:
the "ring"
ZZ
is just modeling the set and it is the implementation of the elements that carry the information of+
and*
.
This distinction is irrelevant because the parent ZZ
is also equipped with a distinguished element class.
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
our parents are equipped with a distinguished category.
... namely, what the method
category()
returns.
That in no way means that any parent is specifically meant to be associated with that category and no other. If that was true, there would be absolutely no reason to have a category
option to Hom
.
I think it is better to treat it like "this is the smallest category where this object (currently) makes sense to belong to."
... this is ill-defined or tautological
No and no. It is well-defined based upon what is implemented. It is not tautological because, e.g., ZZ
is also in Sets()
. Furthermore, saying it is tautological makes no sense because then any definition involving "the smallest" would be tautological.
Replying to @mkoeppe:
Replying to @tscrim:
the "ring"
ZZ
is just modeling the set and it is the implementation of the elements that carry the information of+
and*
.This distinction is irrelevant because the parent
ZZ
is also equipped with a distinguished element class.
In this case, but broadly within Sage, that is completely false. We have many things that have multiple types of element classes. Most (all?) of them are homsets. We don't (yet) have a good system to handle these parents.
Replying to @tscrim:
We have many things that have multiple types of element classes. Most (all?) of them are homsets.
The discussion of parents with multiple elements classes is obviously irrelevant for this discussion.
Replying to @mkoeppe:
Replying to @tscrim:
We have many things that have multiple types of element classes. Most (all?) of them are homsets.
The discussion of parents with multiple elements classes is obviously irrelevant for this discussion.
You just tried to make a point about it. In fact, having a framework for multiple types of elements has been a wishlist item for a while.
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
our parents are equipped with a distinguished category.
... namely, what the method
category()
returns.That in no way means that any parent is specifically meant to be associated with that category and no other.
That's not what "distinguished" means.
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
our parents are equipped with a distinguished category.
... namely, what the method
category()
returns.That in no way means that any parent is specifically meant to be associated with that category and no other.
That's not what "distinguished" means.
If you do not mean it in that way, which is what typically means by "distinguished", then the result of category()
is irrelevant.
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
We have many things that have multiple types of element classes. Most (all?) of them are homsets.
The discussion of parents with multiple elements classes is obviously irrelevant for this discussion.
You just tried to make a point about it. In fact, having a framework for multiple types of elements has been a wishlist item for a while.
Whether you have just one element class or multiple element classes -- the parent is still equipped with the list of the element classes that can be used with it.
And therefore, once more, the distinction of whether the operations are carried in the parent or in the elements is irrelevant.
What's ill-defined is the expression "makes sense to belong to".
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
We have many things that have multiple types of element classes. Most (all?) of them are homsets.
The discussion of parents with multiple elements classes is obviously irrelevant for this discussion.
You just tried to make a point about it. In fact, having a framework for multiple types of elements has been a wishlist item for a while.
Whether you have just one element class or multiple element classes -- the parent is still equipped with the list of the element classes that can be used with it.
And therefore, once more, the distinction of whether the operations are carried in the parent or in the elements is irrelevant.
It is quite important because you care about the extra structure placed on top of ZZ
the set. The parent never models the ring operations because they are invoked with, e.g., __add__
and __mul__
. It might carry the implementation, but you need to construct elements to do the multiplication of elements. If you do not do any +
or *
operations on the elements, you have a set.
Now you are probably going to try and counter with something along the lines of "but equality as rings is different". However, Python/programming equality ==
does not have to be the same as mathematical equality. For example, does 2 == 4 / 2
? I could say no because the QQ
representation is different (provided I don't normalize). So you could have ZZ with a different ring structure, which does not compare as ==
to ZZ
. This is fine because its representation within Sage is different. If we want to compare them as sets (for argument's sake here, consider a finite subset), we have Set
for this.
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
our parents are equipped with a distinguished category.
... namely, what the method
category()
returns.That in no way means that any parent is specifically meant to be associated with that category and no other.
That's not what "distinguished" means.
If you do not mean it in that way, which is what typically means by "distinguished", then the result of
category()
is irrelevant.
Well, the category returned by category()
is certainly not arbitrary. It determines which methods have been mixed into the parent class (and which methods will be mixed into element classes).
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
I think it is better to treat it like "this is the smallest category where this object (currently) makes sense to belong to."
By smallest, do you mean largest?
I mean in smallest the sense of super/sub categories as we have defined the terminology in Sage, which is backwards of how we like to think of with forgetful functors.
Replying to @mkoeppe:
What's ill-defined is the expression "makes sense to belong to".
The implementation with the methods required by the category.
This is exactly what makes it tautological. The category returned category()
defines exactly those methods that have been mixed into the parent.
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
our parents are equipped with a distinguished category.
... namely, what the method
category()
returns.That in no way means that any parent is specifically meant to be associated with that category and no other.
That's not what "distinguished" means.
If you do not mean it in that way, which is what typically means by "distinguished", then the result of
category()
is irrelevant.Well, the category returned by
category()
is certainly not arbitrary. It determines which methods have been mixed into the parent class (and which methods will be mixed into element classes).
However those are just default implementations to reduce code duplication. There are some methods that are ambiguous, the biggest one I can think of is gens()
, which is why we are now advocating for *_generators()
.
Replying to @mkoeppe:
This is exactly what makes it tautological. The category returned
category()
defines exactly those methods that have been mixed into the parent.
If you don't want to consider your object in that category, then you don't call any of those methods. Neither do the methods say what category it belongs to. I could implement ZZ
as a set with an operation coproduct
that could have nothing to do with bialgebras. That doesn't mean it should be treated as a bialgebra.
Replying to @tscrim:
something along the lines of "but equality as rings is different".
Yes, because equality as XYZ is a statement about maps in the category XYZ.
Which is why the forgetful functor when applied to a parent with a distinguished category needs to return a parent with the changed category.
Completely unrelated to the "equality of representations" issues that you talked about.
Replying to @tscrim:
So you could have ZZ with a different ring structure, which does not compare as
==
toZZ
. This is fine because its representation within Sage is different. If we want to compare them as sets (for argument's sake here, consider a finite subset), we haveSet
for this.
Yes, Set(...)
is the forgetful functor, sending the parent with a distinguished category C
to a parent whose distinguished category is a full subcategory of Sets()
.
ForgetfulFunctor(..., Sets())
. That's allReplying to @tscrim:
If you don't want to consider your object in that category, then you don't call any of those methods. Neither do the methods say what category it belongs to.
Exactly, we declare a distinguished category when we call Parent(..., category=...)
-- because it would be ill-defined to guess it from the defined methods.
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
If you do not mean it in that way, which is what typically means by "distinguished", then the result of
category()
is irrelevant.Well, the category returned by
category()
is certainly not arbitrary. It determines which methods have been mixed into the parent class (and which methods will be mixed into element classes).However those are just default implementations to reduce code duplication.
Hardly. It defines and specifies the required methods and even provides tests for these specifications.
Replying to @mkoeppe:
Replying to @tscrim:
something along the lines of "but equality as rings is different".
Yes, because equality as XYZ is a statement about maps in the category XYZ.
Isomorphism is not equality. It is very important not to confuse the two. As soon as you start talking about maps, you are talking isomorphism. In fact, I don't think many category theorists even talk about equality (everything is up to isomorphism).
In fact, that is exactly my point, the forgetful functor is about maps, not the objects. The category for the morphisms should change, but the objects do not need to change. You just no longer want to perform certain operations.
Which is why the forgetful functor when applied to a parent with a distinguished category needs to return a parent with the changed category.
Changing the implementation is not a mathematical operation. It is a programming one. This further reinforces that we should be talking about Python ==
, not mathematical equality. The distinguished category has nothing to do with its mathematical equality statement.
Completely unrelated to the "equality of representations" issues that you talked about.
So ==
should always be mathematical equality, in some definition that you have not made precise? What about the cases when we want this isomorphic to (this occurs in permutation groups) or when no canonical representative exists (e.g., elements in the symbolic ring or for manifolds). We could go for this, but then should we remove code that doesn't comply or things that do not have canonical representatives? If we allow ==
for non-canoical representatives, then what makes those cases special compared to these parents?
Replying to @mkoeppe:
Replying to @tscrim:
So you could have ZZ with a different ring structure, which does not compare as
==
toZZ
. This is fine because its representation within Sage is different. If we want to compare them as sets (for argument's sake here, consider a finite subset), we haveSet
for this.Yes,
Set(...)
is the forgetful functor, sending the parent with a distinguished categoryC
to a parent whose distinguished category is a full subcategory ofSets()
.
I can agree with this statement.
34384 makes this functor available systematically as the
ForgetfulFunctor(..., Sets())
. That's all
We can do this, but that is just some semmantic sugar with making Set()
a distinguished object in the category Sets()
for the ForgetfulFunctor()
. I have no objections. What I do have a strong objection to is saying if it did not do that, then it is a bug.
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
Replying to @tscrim:
If you do not mean it in that way, which is what typically means by "distinguished", then the result of
category()
is irrelevant.Well, the category returned by
category()
is certainly not arbitrary. It determines which methods have been mixed into the parent class (and which methods will be mixed into element classes).However those are just default implementations to reduce code duplication.
Hardly. It defines and specifies the required methods and even provides tests for these specifications.
Yes, you're right here. They do provide specifications for those methods. However, they do also (generally) need to be consistent with things in the super categories because those ParentMethods
will become superclasses. Good programming means we are generally consistent here, but there can be good reason to break that.
Replying to @tscrim:
the forgetful functor is about maps, not the objects. The category for the morphisms should change, but the objects do not need to change. You just no longer want to perform certain operations.
But the objects have a distinguished homset (which is determined by the distinguished category).
Replying to @tscrim:
Completely unrelated to the "equality of representations" issues that you talked about.
So
==
should always be mathematical equality, in some definition that you have not made precise?
The coercion system is in charge of this, it defines (up to implementation restrictions and bugs) our model of "equality".
No changes to it are necessary, nor do I propose changes to it.
Replying to @mkoeppe:
Replying to @tscrim:
the forgetful functor is about maps, not the objects. The category for the morphisms should change, but the objects do not need to change. You just no longer want to perform certain operations.
But the objects have a distinguished homset (which is determined by the distinguished category).
No, they do not. Hom
takes a category
argument to specify the category the morphism belongs to:
sage: Hom(QQ, ZZ)
Set of Homomorphisms from Rational Field to Integer Ring
sage: Hom(QQ, ZZ, category=Sets())
Set of Morphisms from Rational Field to Integer Ring in Category of sets
Replying to @tscrim:
34384 makes this functor available systematically as the
ForgetfulFunctor(..., Sets())
. That's allWe can do this, but that is just some semantic sugar
... that's a new one, yes, functors do provide semantic sugar which enables composability.
Replying to @tscrim:
Replying to @mkoeppe:
But the objects have a distinguished homset (which is determined by the distinguished category).
No, they do not.
Hom
takes acategory
argument to specify the category the morphism belongs to:sage: Hom(QQ, ZZ) Set of Homomorphisms from Rational Field to Integer Ring
Here the distinguished category determined the default.
Replying to @mkoeppe:
Replying to @tscrim:
Completely unrelated to the "equality of representations" issues that you talked about.
So
==
should always be mathematical equality, in some definition that you have not made precise?The coercion system is in charge of this, it defines (up to implementation restrictions and bugs) our model of "equality".
Not for elements of the same parent or between different parents.
No changes to it are necessary, nor do I propose changes to it.
You are implicitly changing the requirements (and hence, the definition) by calling something a bug that IMSO should not be (nor has it previously been considered to be AFAIK).
If I have a new ring structure on Z, lets call it TT
, then ZZ == TT
would be False
. Now if we apply the forgetful functor to them, they would still be false. I am saying this is fine and proper because they are different representations of the same set, but you are saying it is a bug.
Replying to @tscrim:
You are implicitly changing the requirements (and hence, the definition) by calling something a bug that IMSO should not be (nor has it previously been considered to be AFAIK).
Read https://github.com/sagemath/sage-prod/blob/develop/src/sage/categories/sets_cat.py#L254
Replying to @mkoeppe:
Replying to @tscrim:
Replying to @mkoeppe:
But the objects have a distinguished homset (which is determined by the distinguished category).
No, they do not.
Hom
takes acategory
argument to specify the category the morphism belongs to:sage: Hom(QQ, ZZ) Set of Homomorphisms from Rational Field to Integer Ring
Here the distinguished category determined the default.
Mathematically, any homset should also take the category as input. We just usually suppress it from notion as context determines, just like what is done here. Now I will concede that applying the forgetful functor should make it clear what category we want to do it in, but that is just breaking the syntactic (semantic?) sugar. You would rather try to solve an impossible problem than simply telling the user to type fewer characters with a more intuitive semantic?
Replying to @tscrim:
If I have a new ring structure on Z, lets call it
TT
, thenZZ == TT
would beFalse
. Now if we apply the forgetful functor to them, they would still be false. I am saying this is fine and proper because they are different representations of the same set
I wouldn't say that it's fine and proper; I'd say there is an implementation restriction that stops us from detecting that they are equal. Sage is of course full of such implementation restrictions.
A proper implementation of the forgetful functor will improve this situation: It removes the implementation restriction.
Replying to @tscrim:
Now I will concede that applying the forgetful functor should make it clear what category we want to do it in
Yes, in particular because after applying it, the distinguished category has changed. We can just call the method .category()
if we want to know it.
Replying to @mkoeppe:
Replying to @tscrim:
You are implicitly changing the requirements (and hence, the definition) by calling something a bug that IMSO should not be (nor has it previously been considered to be AFAIK).
Read https://github.com/sagemath/sage-prod/blob/develop/src/sage/categories/sets_cat.py#L254
This is not evidence of anything about a general bug for ForgetfulFunctor
. That is about this category's implementation of __call__
, which is what ForgetfulFunctor
does. If F: C -> D
, then F(X)
is performs D(x)
. It is up to each category to impose how it wants to perform its __call__
, which is an implementation detail.
We have currently the following bug:
The result should rather be:
so that:
F(ZZ).category()
givesInfiniteEnumeratedSets()
Hom(F(ZZ), ...)
will construct morphisms in the correct categoryTickets:
34384 Proper
ForgetfulFunctor
withcodomain=Sets()
or its full subcategories34390
ForgetfulFunctor
: Register coercions34402 Proper
ForgetfulFunctor
withcodomain=Modules(ZZ)
viaAdditiveAbelianGroupWrapper
31241 Forgetful Functors for Manifold Objects
CC: @tscrim @mkoeppe @tobiasdiez @nthiery @simon-king-jena @hivert
Component: categories
Issue created by migration from https://trac.sagemath.org/ticket/31247