Faced with the problem to implement elements of the rings Z/nZ, we must define the types of these elements as far as is necessary to distinguish them from other GAP objects.
As is described in Chapter Types of Objects in the Reference Manual, the type of an object comprises several aspects of information about this object; the family determines the relation of the object to other objects, the categories determine what operations the object admits, the representation determines how an object is actually represented, and the attributes describe knowledge about the object.
First of all, we must decide about the family of each residue class. A natural way to do this is to put the elements of each ring Z/nZ into a family of their own. This means that for example elements of Z/3Z and Z/9Z lie in different families. So the only interesting relation between the families of two residue classes is equality; binary arithmetic operations with two residue classes will be admissible only if their families are equal. Note that in the naive approach in Section A First Attempt to Implement Elements of Residue Class Rings, we had to take care of different moduli by a check in each function; these checks may disappear in the new approach because of our choice of families.
Note that we do not need to tell GAP anything about the above
decision concerning the families of the objects that we are going to
implement,
that is, the declaration part (see Declaration and Implementation Part)
of the little GAP package we are writing contains nothing about the
distribution of the new objects into families.
(The actual construction of a family happens in the function MyZmodnZ
shown below.)
Second, we want to describe methods to add or multiply two elements in Z/nZ, and these methods shall be not applicable to other GAP objects. The natural way to do this is to create a new category in which all elements of all rings Z/nZ lie. This is done as follows.
DeclareCategory( "IsMyZmodnZObj", IsScalar ); DeclareCategoryCollections( "IsMyZmodnZObj" );So all elements in the rings Z/nZ will lie in the category
IsMyZmodnZObj, which is a subcategory of IsScalar.
The latter means that one can add, subtract, multiply and divide
two such elements that lie in the same family,
with the obvious restriction that the second operand of a division
must be invertible.
(The name IsMyZmodnZObj is chosen because IsZmodnZObj is already
defined in GAP, for an implementation of residue classes that is
very similar to the one developed in this manual chapter.
Using this different name, one can simply enter the GAP code of this
chapter into a GAP session, either interactively or by reading a file
with this code, and experiment after each step whether the expected
behaviour has been achieved, and what is still missing.)
The second line of GAP code above declares the category
CategoryCollections( IsMyZmodnZObj ), which will be needed later;
it is important to create this category before collections of the objects
in IsMyZmodnZObj arise.
Note that the only difference between DeclareCategory and NewCategory
is that in a call to DeclareCategory, a variable corresponding to the
first argument is set to the new category, and this variable is read-only
(see Global Variables in the Library).
The same holds for DeclareRepresentation and NewRepresentation etc.
There is no analogue of categories in the implementation in Section A First Attempt to Implement Elements of Residue Class Rings, since there it was not necessary to distinguish residue classes from other GAP objects. Note that the functions there assumed that their arguments were residue classes, and the user was responsible not to call them with other arguments. Thus an important aspect of types is to describe arguments of functions explicitly.
Third, we must decide about the representation of our objects.
This is something we know already from
Section A First Attempt to Implement Elements of Residue Class Rings,
where we chose a list of length two.
Here we may choose between two essentially different representations for
the new GAP objects, namely as ``component object'' (record--like)
or ``positional object'' (list--like).
We decide to store the modulus of each residue class in its family,
and to encode the element k + nZ by the unique residue in the range
[ 0 .. n-1 ] that is congruent to k modulo n,
and the object itself is chosen to be a positional object with this
residue at the first and only position (see Positional Objects).
DeclareRepresentation( "IsMyModulusRep", IsPositionalObjectRep, [ 1 ] );
The fourth ingredients of a type, attributes, are usually of minor importance for element objects. In particular, we do not need to introduce special attributes for residue classes.
Having defined what the new objects shall look like, we now declare a global function (see Declaration and Implementation Part), to create an element when family and residue are given.
DeclareGlobalFunction( "MyZmodnZObj" );
Now we have declared what we need, and we can start to implement the missing methods resp. functions; so the following command belongs to the implementation part of our package (see Declaration and Implementation Part).
The probably most interesting function is the one to construct a residue class.
InstallGlobalFunction( MyZmodnZObj, function( Fam, residue )
return Objectify( NewType( Fam, IsMyZmodnZObj and IsMyModulusRep ),
[ residue mod Fam!.modulus ] );
end );
Note that we normalize residue explicitly using mod;
we assumed that the modulus is stored in Fam,
so we must take care of this below.
If Fam is a family of residue classes, and residue is an integer,
MyZmodnZObj returns the corresponding object in the family Fam,
which lies in the category IsMyZmodnZObj and in the representation
IsMyModulusRep.
MyZmodnZObj needs an appropriate family as first argument,
so let us see how to get our hands on this.
Of course we could write a handy function to create such a family
for given modulus, but we choose another way.
In fact we do not really want to call MyZmodnZObj explicitly when we
want to create residue classes.
For example, if we want to enter a matrix of residues then usually
we start with a matrix of corresponding integers,
and it is more elegant to do the conversion via multiplying the matrix
with the identity of the required ring Z/nZ;
this is also done for the conversion of integral matrices to
finite field matrices.
(Note that we will have to install a method for this.)
So it is often sufficient to access this identity,
for example via One( MyZmodnZ( n ) ),
where MyZmodnZ returns a domain representing the ring Z/nZ
when called with the argument n.
We decide that constructing this ring is a natural place where the
creation of the family can be hidden,
and implement the function.
(Note that the declaration belongs to the declaration part,
and the installation belongs to the implementation part,
see Declaration and Implementation Part).
DeclareGlobalFunction( "MyZmodnZ" );
InstallGlobalFunction( MyZmodnZ, function( n )
local F, R;
if not IsPosInt( n ) then
Error( "<n> must be a positive integer" );
fi;
# Construct the family of element objects of our ring.
F:= NewFamily( Concatenation( "MyZmod", String( n ), "Z" ),
IsMyZmodnZObj );
# Install the data.
F!.modulus:= n;
# Make the domain.
R:= RingWithOneByGenerators( [ MyZmodnZObj( F, 1 ) ] );
SetIsWholeFamily( R, true );
SetName( R, Concatenation( "(Integers mod ", String(n), ")" ) );
# Return the ring.
return R;
end );
Note that the modulus n is stored in the component modulus of the
family, as is assumed by MyZmodnZ.
Thus it is not necessary to store the modulus in each element.
When storing n with the !. operator as value of the component
modulus, we used that all families are in fact represented as
component objects (see Component Objects).
We see that we can use RingWithOneByGenerators to construct a ring
with one if we have the appropriate generators.
The construction via RingWithOneByGenerators makes sure
that IsRingWithOne (and IsRing) is true for each output of MyZmodnZ.
So the main problem is to create the identity element of the ring,
which in our case suffices to generate the ring.
In order to create this element via MyZmodnZObj,
we have to construct its family first, at each call of MyZmodnZ.
Also note that we may enter known information about the ring.
Here we store that it contains the whole family of elements;
this is useful for example when we want to check the membership of an
element in the ring, which can be decided from the type of the element
if the ring contains its whole elements family.
Giving a name to the ring causes that it will be printed
via printing the name.
(By the way:
This name (Integers mod n) looks like a call to \mod with the
arguments Integers and n;
a construction of the ring via this call seems to be more natural than
by calling MyZmodnZ; later we shall install a \mod method in order
to admit this construction.)
Now we can read the above code into GAP, and the following works already.
gap> R:= MyZmodnZ( 4 ); (Integers mod 4) gap> IsRing( R ); true gap> gens:= GeneratorsOfRingWithOne( R ); [ <object> ]But of course this means just to ask for the information we have explicitly stored in the ring. Already the questions whether the ring is finite and how many elements it has, cannot be answered by GAP. Clearly we know the answers, and we could store them in the ring, by setting the value of the property
IsFinite to true and the value
of the attribute Size to n (the argument of the call to MyZmodnZ).
If we do not want to do so then GAP could only try to find out the number
of elements of the ring via forming the closure of the generators
under addition and multiplication,
but up to now, GAP does not know how to add or multiply two
elements of our ring.
So we must install some methods for arithmetic and other operations if the elements are to behave as we want.
We start with a method for showing elements nicely on the screen.
There are different operations for this purpose.
One of them is PrintObj, which is called for each argument in an
explicit call to Print.
Another one is ViewObj, which is called in the read-eval-print loop
for each object.
ViewObj shall produce short and human readable information about the
object in question, whereas PrintObj shall produce information that
may be longer and is (if reasonable) readable by GAP.
We cannot satisfy the latter requirement for a PrintObj method
because there is no way to make a family GAP readable.
So we decide to display the expression ( k mod n ) for an object
that is given by the residue k and the modulus n,
which would be fine as a ViewObj method.
Since the default for ViewObj is to call PrintObj,
and since no other ViewObj method is applicable to our elements,
we need only a PrintObj method.
InstallMethod( PrintObj, "for element in Z/nZ (ModulusRep)", [ IsMyZmodnZObj and IsMyModulusRep ], function( x ) Print( "( ", x![1], " mod ", FamilyObj(x)!.modulus, " )" ); end );
So we installed a method for the operation PrintObj (first argument),
and we gave it a suitable information message (second argument),
see ApplicableMethod and Tracing Methods for applications of
this information string.
The third argument tells GAP that the method is applicable for
objects that lie in the category IsMyZmodnZObj and in the representation
IsMyModulusRep.
and the fourth argument is the method itself.
More details about InstallMethod can be found in Method Installation.
Note that the requirement IsMyModulusRep for the argument x allows us
to access the residue as x![1].
Since the family of x has the component modulus bound if it is
constructed by MyZmodnZ, we may access this component.
We check whether the method installation has some effect.
gap> gens; [ ( 1 mod 4 ) ]
Next we install methods for the comparison operations. Note that we can assume that the residues in the representation chosen are normalized.
InstallMethod( \=,
"for two elements in Z/nZ (ModulusRep)",
IsIdenticalObj,
[ IsMyZmodnZObj and IsMyModulusRep, IsMyZmodnZObj and IsMyModulusRep ],
function( x, y ) return x![1] = y![1]; end );
InstallMethod( \<,
"for two elements in Z/nZ (ModulusRep)",
IsIdenticalObj,
[ IsMyZmodnZObj and IsMyModulusRep, IsMyZmodnZObj and IsMyModulusRep ],
function( x, y ) return x![1] < y![1]; end );
The third argument used in these installations specifies the required
relation between the families of the arguments
(see Families in the Reference Manual).
This argument of a method installation, if present, is a function that shall
be applied to the families of the arguments.
IsIdenticalObj means that the methods are applicable only if both arguments
lie in the same family.
(In installations for unary methods, obviously no relation is required,
so this argument is left out there.)
Up to now, we see no advantage of the new approach over the one in
Section A First Attempt to Implement Elements of Residue Class Rings.
For a residue class represented as [ k, n ], the way it is printed
on the screen is sufficient, and equality and comparison of lists are
good enough to define equality and comparison of residue classes if needed.
But this is not the case in other situations.
For example, if we would have decided that the residue k need not be
normalized then we would have needed functions in
Section A First Attempt to Implement Elements of Residue Class Rings
that compute whether two residue classes are equal, and which of two
residue classes is regarded as larger than another.
Note that we are free to define what ``larger'' means for objects that
are newly introduced.
Next we install methods for the arithmetic operations, first for the additive structure.
InstallMethod( \+,
"for two elements in Z/nZ (ModulusRep)",
IsIdenticalObj,
[ IsMyZmodnZObj and IsMyModulusRep, IsMyZmodnZObj and IsMyModulusRep ],
function( x, y )
return MyZmodnZObj( FamilyObj( x ), x![1] + y![1] );
end );
InstallMethod( ZeroOp,
"for element in Z/nZ (ModulusRep)",
[ IsMyZmodnZObj ],
x -> MyZmodnZObj( FamilyObj( x ), 0 ) );
InstallMethod( AdditiveInverseOp,
"for element in Z/nZ (ModulusRep)",
[ IsMyZmodnZObj and IsMyModulusRep ],
x -> MyZmodnZObj( FamilyObj( x ), AdditiveInverse( x![1] ) ) );
Here the new approach starts to pay off.
The method for the operation \+ allows us to use the infix
operator + for residue classes.
The method for ZeroOp is used when we call this operation or the
attribute Zero explicitly,
and ZeroOp it is also used when we ask for 0 * rescl,
where rescl is a residue class.
(Note that Zero and ZeroOp are distinguished
because 0 * obj is guaranteed to return a mutable result whenever
a mutable version of this result exists in GAP --for example if obj
is a matrix-- whereas Zero is an attribute and therefore returns
immutable results;
for our example there is no difference since the residue classes are
always immutable,
nevertheless we have to install the method for ZeroOp.
The same holds for AdditiveInverse, One, and Inverse.)
Similarly, AdditiveInverseOp can be either called directly or via the
unary - operator; so we can compute the additive inverse of the
residue class rescl as -rescl.
It is not necessary to install methods for subtraction, since this is handled via addition of the additive inverse of the second argument if no other method is installed.
Let us try what we can do with the methods that are available now.
gap> x:= gens[1]; y:= x + x; ( 1 mod 4 ) ( 2 mod 4 ) gap> 0 * x; -x; ( 0 mod 4 ) ( 3 mod 4 ) gap> y = -y; x = y; x < y; -x < y; true false true false
We might want to admit the addition of integers and elements in
rings Z/nZ, where an integer is implicitly identified
with its residue modulo n.
To achieve this, we install methods to add an integer to an object in
IsMyZmodnZObj from the left and from the right.
InstallMethod( \+,
"for element in Z/nZ (ModulusRep) and integer",
[ IsMyZmodnZObj and IsMyModulusRep, IsInt ],
function( x, y )
return MyZmodnZObj( FamilyObj( x ), x![1] + y );
end );
InstallMethod( \+,
"for integer and element in Z/nZ (ModulusRep)",
[ IsInt, IsMyZmodnZObj and IsMyModulusRep ],
function( x, y )
return MyZmodnZObj( FamilyObj( y ), x + y![1] );
end );
Now we can do also the following.
gap> 2 + x; 7 - x; y - 2; ( 3 mod 4 ) ( 2 mod 4 ) ( 0 mod 4 )
Similarly we install the methods dealing with the multiplicative
structure.
We need methods to multiply two of our objects,
and to compute identity and inverse.
The operation OneOp is called when we ask for rescl^0,
and InverseOp is called when we ask for rescl^-1.
Note that the method for InverseOp returns fail if the argument
is not invertible.
InstallMethod( \*,
"for two elements in Z/nZ (ModulusRep)",
IsIdenticalObj,
[ IsMyZmodnZObj and IsMyModulusRep, IsMyZmodnZObj and IsMyModulusRep ],
function( x, y )
return MyZmodnZObj( FamilyObj( x ), x![1] * y![1] );
end );
InstallMethod( OneOp,
"for element in Z/nZ (ModulusRep)",
[ IsMyZmodnZObj ],
elm -> MyZmodnZObj( FamilyObj( elm ), 1 ) );
InstallMethod( InverseOp,
"for element in Z/nZ (ModulusRep)",
[ IsMyZmodnZObj and IsMyModulusRep ],
function( elm )
local residue;
residue:= QuotientMod( 1, elm![1], FamilyObj( elm )!.modulus );
if residue <> fail then
residue:= MyZmodnZObj( FamilyObj( elm ), residue );
fi;
return residue;
end );
To be able to multiply our objects with integers, we need not (but we may, and we should if we are going for efficiency) install special methods. This is because in general, GAP interprets the multiplication of an integer and an additive object as abbreviation of successive additions, and there is one generic method for such a multiplication that uses only additions and ---in the case of a negative integer--- taking the additive inverse. Analogously, there is a generic method for powering by integers that uses only multiplications and taking the multiplicative inverse.
Note that we could also interpret the multiplication with an integer as a shorthand for the multiplication with the corresponding residue class. We are lucky that this interpretation is compatible with the one that is already available. If this would not be the case then of course we would get into trouble by installing a concurrent multiplication that computes something different from the multiplication that is already defined, since GAP does not guarantee which of the applicable methods is actually chosen (see Applicable Methods and Method Selection).
Now we have implemented methods for the arithmetic operations for our elements, and the following calculations work.
gap> y:= 2 * x; z:= (-5) * x; ( 2 mod 4 ) ( 3 mod 4 ) gap> y * z; y * y; ( 2 mod 4 ) ( 0 mod 4 ) gap> y^-1; y^0; fail ( 1 mod 4 ) gap> z^-1; ( 3 mod 4 )
There are some other operations in GAP that we may want to accept
our elements as arguments.
An example is the operation Int that returns, e.g.,
the integral part of a rational number or the integer corresponding to
an element in a finite prime field.
For our objects, we may define that Int returns the normalized residue.
Note that we define this behaviour for elements
but we implement it for objects in the representation IsMyModulusRep.
This means that if someone implements another representation of
residue classes then this person must be careful to implement Int
methods for objects in this new representation compatibly with our
definition, i.e., such that the result is independent of the representation.
InstallMethod( Int,
"for element in Z/nZ (ModulusRep)",
[ IsMyZmodnZObj and IsMyModulusRep ],
z -> z![1] );
Another example of an operation for which we might want to install
a method is \mod.
We make the ring print itself as Integers mod the modulus,
and then it is reasonable to allow a construction this way,
which makes the PrintObj output of the ring GAP readable.
InstallMethod( PrintObj,
"for full collection Z/nZ",
[ CategoryCollections( IsMyZmodnZObj ) and IsWholeFamily ],
function( R )
Print( "(Integers mod ",
ElementsFamily( FamilyObj(R) )!.modulus, ")" );
end );
InstallMethod( \mod,
"for `Integers', and a positive integer",
[ IsIntegers, IsPosRat and IsInt ],
function( Integers, n ) return MyZmodnZ( n ); end );
Let us try this.
gap> Int( y ); 2 gap> Integers mod 1789; (Integers mod 1789)
Probably it is not necessary to emphasize that with the approach of
Section A First Attempt to Implement Elements of Residue Class Rings,
installing methods for existing operations is usually not possible or
at least not recommended.
For example, installing the function resclass_sum defined in
Section A First Attempt to Implement Elements of Residue Class Rings
as a \+ method for adding two lists of length two
(with integer entries) would not be compatible with the general
definition of the addition of two lists of same length.
Installing a method for the operation Int that takes a list
[ k, n ] and returns k would in principle be possible,
since there is no Int method for lists yet,
but it is not sensible to do so because one can think of other
interpretations of such a list where different Int methods could
be installed with the same right.
As mentioned in Section Why Proceed in a Different Way?, one advantage of the new approach is that with the implementation we have up to now, automatically also matrices of residue classes can be treated.
gap> r:= Integers mod 16; (Integers mod 16) gap> x:= One( r ); ( 1 mod 16 ) gap> mat:= IdentityMat( 2 ) * x; [ [ ( 1 mod 16 ), ( 0 mod 16 ) ], [ ( 0 mod 16 ), ( 1 mod 16 ) ] ] gap> mat[1][2]:= x;; gap> mat; [ [ ( 1 mod 16 ), ( 1 mod 16 ) ], [ ( 0 mod 16 ), ( 1 mod 16 ) ] ] gap> Order( mat ); 16 gap> mat + mat; [ [ ( 2 mod 16 ), ( 2 mod 16 ) ], [ ( 0 mod 16 ), ( 2 mod 16 ) ] ] gap> last^4; [ [ ( 0 mod 16 ), ( 0 mod 16 ) ], [ ( 0 mod 16 ), ( 0 mod 16 ) ] ]
Such matrices, if they are invertible, are valid as group elements. So we can also construct matrix groups over residue class rings.
gap> mat2:= IdentityMat( 2 ) * x;; gap> mat2[2][1]:= x;; gap> g:= Group( mat, mat2 );; gap> Size( g ); 3072 gap> Factors( last ); [ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3 ] gap> syl3:= SylowSubgroup( g, 3 );; gap> gens:= GeneratorsOfGroup( syl3 ); [ [ [ ( 8 mod 16 ), ( 5 mod 16 ) ], [ ( 11 mod 16 ), ( 7 mod 16 ) ] ] ] gap> Order( gens[1] ); 3
By the way, also groups of (invertible) residue classes can be formed, but this may be of minor interest.
gap> g:= Group( x ); Size( g ); <group with 1 generators> 1 gap> g:= Group( 3*x ); Size( g ); <group with 1 generators> 4
Having done enough for the elements, we may install some more methods for the rings if we want to use them as arguments. These rings are finite, and there are many generic methods that will work if they are able to compute the list of elements of the ring, so we install a method for this.
InstallMethod( Enumerator,
"for full collection Z/nZ",
[ CategoryCollections( IsMyZmodnZObj ) and IsWholeFamily ],
function( R )
local F;
F:= ElementsFamily( FamilyObj(R) );
return List( [ 0 .. Size( R ) - 1 ], x -> MyZmodnZObj( F, x ) );
end );
Note that this method is applicable only to full rings Z/nZ, for proper subrings it would return a wrong result. Furthermore, it is not required that the argument is a ring; in fact this method is applicable also to the additive group formed by all elements in the family, provided that it knows to contain the whole family.
Analogously, we install methods to compute the size, a random element, and the units of full rings Z/nZ.
InstallMethod( Random,
"for full collection Z/nZ",
[ CategoryCollections( IsMyZmodnZObj ) and IsWholeFamily ],
R -> MyZmodnZObj( ElementsFamily( FamilyObj(R) ),
Random( [ 0 .. Size( R ) - 1 ] ) ) );
InstallMethod( Size,
"for full ring Z/nZ",
[ CategoryCollections( IsMyZmodnZObj ) and IsWholeFamily ],
R -> ElementsFamily( FamilyObj(R) )!.modulus );
InstallMethod( Units,
"for full ring Z/nZ",
[ CategoryCollections( IsMyZmodnZObj )
and IsWholeFamily and IsRing ],
function( R )
local F;
F:= ElementsFamily( FamilyObj( R ) );
return List( PrimeResidues( Size(R) ), x -> MyZmodnZObj( F, x ) );
end );
The Units method has the disadvantage that the result is returned
as a list (in fact this list is also strictly sorted).
We could improve the implementation by returning the units as a group;
if we do not want to take the full list of elements as generators,
we can use the function GeneratorsPrimeResidues
(see GeneratorsPrimeResidues in the Reference Manual).
InstallMethod( Units,
"for full ring Z/nZ",
[ CategoryCollections( IsMyZmodnZObj )
and IsWholeFamily and IsRing ],
function( R )
local G, gens;
gens:= GeneratorsPrimeResidues( Size( R ) ).generators;
if not IsEmpty( gens ) and gens[ 1 ] = 1 then
gens:= gens{ [ 2 .. Length( gens ) ] };
fi;
gens:= Flat( gens ) * One( R );
return GroupByGenerators( gens, One( R ) );
end );
Each ring Z/nZ is finite,
and we could install a method that returns true when IsFinite is
called with Z/nZ as argument.
But we can do this more elegantly via installing a logical implication.
InstallTrueMethod( IsFinite,
CategoryCollections( IsMyZmodnZObj ) and IsDomain );
In effect, every domain that consists of elements in IsMyZmodnZObj
will automatically store that it is finite,
even if IsFinite is not called for it.
[Top] [Previous] [Up] [Next] [Index]
GAP 4 manual