On 8 Jun., 07:04, Robert Bradshaw <rober...@math.washington.edu
> > One caveat, though. The standard "Ring" and "Algebra" parents both
> > inherit from the ParentWithBase. Am I allowed to mix an element class
> > derived from AlgebraElements with a parent class derived from Parent?
> I can't say for sure that there aren't any hidden assumptions, but I
> don't think the Parent's class hierarchy constrains the Element's.
I agree with Robert. As much as I understand, ring elements do not do
any special assumption on their parents.
> > Also, is it my impression or this model seems to require a lot of
> > boilerplate if I wanted to implement it fully (with categories,
> > functors, etc)?
I didn't mention functors, although I love the stuff in
The full "boilerplate", if you want to do all of coercion model and
category framework, should roughly be as described below.
Let MyParent be the parent class and MyElement a class for the
elements of a MyParent instance. Let P and E denote instances of
MyParent and MyElement, respectively.
I start with a summary of the steps, telling how important and how
difficult they usually are, IMO:
0. Classes to inherit from: Easy, HAS to be done.
1. "Magic" Python methods: Mainly easy, you just need to write _repr_
instead of __repr__, etc. __cmp__ may be a bit more tricky.
2.a) Category for parents. Mainly easy: Just choose a category and
initialise your parent with it. Should/Has to be done.
2.b) Category for elements: Mainly easy. Just do
Element = MyElement
Should be done, if you use Python or wait until #11115 is merged.
You may be required to provide certain element or parent methods, but
that depends on the category you chose.
3. Basic coercion. Can range from "nothing to do at all" to "tedious".
It is a must-have!
3.a) Conversion: 2.b) may actually be sufficient for it, otherwise
3.b) Coercion maps: Done with implementing one method, namely
4. Not-so-basic coercion with functors. Tricky, can be seen as cherry
on the cake (so, not necessary but sometimes nice to have).
Here are the gory details.
0. Classes to inherit from
MyParent should inherit from sage.structure.parent.Parent and
MyElement from sage.structure.element.Element, or of course from a sub-
1. "Magic" Python methods
Do not use __repr__, __add__, __mul__ etc with double underscore, but
with single underscore (_repr_, _add_,_mul_). The double underscore
methods are inherited, and the generic code should not be overridden
since it provides coercion. The case of __cmp__ is a bit more
complicated -- please read the comments in the vicinity of
2. Category framework
2.a) Initialisation of the parent class
In MyParent.__init__, you should construct a category (or pass it as
an argument to __init__), and you should call
Parent.__init__(self,...,category=your_category). That is usually a
small amount of work, since probably you will find an existing
category that suites your needs --- Sets(), Algebras(basering),
Rngs(), Rings(), etc.
NOTE: If MyParent is written in Python, then you will find that its
instances belong to a class MyParent_with_category -- that is a
subclass of MyParent that is automatically obtained by adding stuff
from the category framework
2.b) Initialisation of the element class
The class MyParent should have an attribute called `Element`, and its
value should be MyElement. The background is that the attribute
Element is automatically mixed with stuff from the category and turned
into a new class available as P.element_class (where P is an instance
of MyParent). Note that this only works if MyParent is written in
Python --- or you may apply #11115, because then it also works with
2.c) Required abstract methods
Some categories require that you implement certain methods for
MyParent or MyElement, such as lift() if your parent is a quotient.
2.d) Make the test suites work
You are supposed to provide doc tests of the form TestSuite(P).run().
To make them work, you may need to implement further things, like
MyParent._an_element_. The error messages of TestSuite may tell you
what is missing. But you may actually be lucky, sometimes there are
generic methods that do the job for you.
3. Coercion - the basics
3.a) Element conversion
Note that this is CONVERSION. So, the aim is that P(bla) returns an
element of P to the given argument bla -- but it is not necessarily
the case that there is a coercion map from parent(bla) to P.
3.a).(i) The default
It may be enough to do *nothing* at all: You provided MyParent.Element
= MyElement in step 2.b), and the default is that P(bla) returns
P.element_class(bla), and that will use the initialisation from
MyElement like MyElement(P,bla). So, if MyElement.__init__ is able to
understand anything convertible to P then you can rely on the default!
If you want to keep MyElement.__init__ simple, then you should provide
MyParent with a method _element_constructor_. It should transform
given arguments to something you can use to initialise MyElement. BUT
NOTE: You should NOT return MyElement(P,...)!! Instead, return
3.c) Coercion maps
A coercion of "bla" into P is more than just a conversion P(bla),
because in a coercion you must have a structure preserving map from
the parent of bla to P. Think of the integers: You can convert 1.0
into ZZ, but there is no coercion from RR (the parent of 1.0) to ZZ.
You should provide MyParent with a method _coerce_map_from_.
Requirements: If there is a coercion from a parent S to P, then
P._coerce_map_from_(S) should either return True or an actual map from
S to P.
(i) If it returns a map f, then an element e of S is coerced into
the element f(e) of P
(ii) If it just returns True, then P.coerce_map_from(S) will
automatically create a map for you. Coercion of an element e of S into
P boils down to calling P._element_constructor_(e).
4. Coercion -- the advanced stuff.
This step is needed if you want to do arithmetic with elements from P
and elements from S, where neither S coerces into P nor P coerces into
S. A typical example is P=ZZ[x] and S=QQ. The result of an arithmetic
operation between S and P lives in QQ[x], hence, neither in S nor in
4.a) The construction of P
It is assumed that you can construct P out of a simpler parent by
means of a construction that is supposed to be functorial (e.g., the
construction of "forming a polynomial ring with variable x" transforms
*any* ring R into a ring R[x], and any map from R to S yields a map
from R[x] to S[x]).
Then, you should provide MyParent with a method construction(), that
returns a pair F,R, where F is a so-called construction functor, and
F(R) == P.
4.b) The construction functor F
See the examples in sage.categories.pushout. Basically, you need to
implement a class MyFunctor inherited from
sage.categories.pushout.ConstructionFunctor, and provide it with a
4.c) Pushout of functors
Assume that F1 is an instance of MyFunctor and F2 is any other
construction functor, and assume that there is neither a coercion from
F1(R) to F2(R) nor from F2(R) to F1(R). Assume further that you can
think of a "canonical" parent S that can be obtained from R, such that
both F1(R) and F2(R) coerce into S. Then, F1.pushout(F2) should return
a functor F3 such that S==F3(R).
Example from above: F1 is the fraction field constructor, F2 is the
polynomial ring constructor with variable x, and R is ZZ. Then,
F1(R)==QQ and F2(R)==ZZ[x] . You want S = QQ[x], hence, F3 = F2(F1),
such that S==F3(R)=F2(F1(R)).
Fortunately, this can be achieved very easily, using the default
method pushout() of ConstructionFunctor. You just provide MyFunctor
with an attribute "rank". If F1.rank is smaller than F2.rank then F3
will be "first F1, then F2". Example:
sage: R = ZZ
sage: F1 = QQ.construction()
sage: F2 = R['x'].construction()
4.d) Merging functors
This is when functors F1 and F2 are of the same rank.
It may be that your parent comes in different implementations. For
example, ZZ[x] can be in a dense or sparse implementation, based on
NTL or FLINT. The construction functor should know about these
details. And then, MyFunctor should be provided with a method merge()
with the following properties: If F1 and F2 are two construction
functors of the same rank, such that F1(R) and F2(R) are isomorphic
objects in different implementation for ANY R, then F1.merge(F2)
should return a functor F3 such that F3(R) is isomophic to F1(R) and
F2(R) and there is a coercion from both F1(R) and F2(R) to F3(R).
Otherwise, None should be returned.
Example: If F1 returns dense 3x3 matrix spaces and F2 returns sparse
3x3 matrix spaces, and you decide that dense is the default, then
F1.merge(F2) should return F1.
sage: MD = MatrixSpace(QQ,3,3,sparse=False)
sage: MS = MatrixSpace(QQ,3,3,sparse=True)
So, if you want the full programme then it's much to do. But often,
well-chosen parts of it are sufficient.