In this section we will show how one can add a new type of group elements to GAP. A lot of group elements in GAP are implemented this way, for example elements of generic factor groups, or elements of generic direct products.
We will use prime residue classes modulo an integer as our example. They have the advantage that the arithmetic is very simple, so that we can concentrate on the implementation without being carried away by mathematical details.
Note that everything we define is already in the library in the file
LIBNAME/"numtheor.g"
, so there is no need for you to type it in. You
may however like to make a copy of this file and modify it.
We will represent residue classes by records. This is absolutely typical, all group elements not built into the GAP kernel are realized by records.
To distinguish records representing residue classes from other records we
require that residue class records have a component with the name
isResidueClass
and the value true
. We also require that they have a
component with the name isGroupElement
and again the value true
.
Those two components are called the tag components.
Next each residue class record must of course have components that tell
us which residue class this record represents. The component with the
name representative
contains the smallest nonnegative element of the
residue class. The component with the name modulus
contains the
modulus. Those two components are called the identifying components.
Finally each residue class record must have a component with the name
operations
that contains an appropriate operations record (see below).
In this way we can make use of the possibility to define operations for
records (see Comparisons of Records and Operations for Records).
Below is an example of a residue class record.
r13mod43 := rec( isGroupElement := true, isResidueClass := true, representative := 13, modulus := 43, domain := GroupElements, operations := ResidueClassOps );
The first function that we have to write is very simple. Its only task
is to test whether an object is a residue class. It does this by testing
for the tag component isResidueClass
.
gap> IsResidueClass := function ( obj ) > return IsRec( obj ) > and IsBound( obj.isResidueClass ) > and obj.isResidueClass; > end;;
Our next function takes a representative and a modulus and constructs a new residue class. Again this is not very difficult.
gap> ResidueClass := function ( representative, modulus ) > local res; > res := rec(); > res.isGroupElement := true; > res.isResidueClass := true; > res.representative := representative mod modulus; > res.modulus := modulus; > res.domain := GroupElements; > res.operations := ResidueClassOps; > return res; > end;;
Now we have to define the operations record for residue classes.
Remember that this record contains a function for each binary operation,
Comparisons of Records and Operations for Records). The operations =
, <
, *
,
/
, mod
, ^
, Comm
, and Order
are the ones that are applicable to
all group elements. The meaning of those operations for group elements
Operations for Group Elements.
Luckily we do not have to define everything. Instead we can inherit a
lot of those functions from generic group elements. For example, for all
group elements g/h
should be equivalent to g*h^-1
. So the
function for /
could simply be function(g,h) return g*h^-1; end
.
Note that this function can be applied to all group elements,
independently of their type, because all the dependencies are in *
and
^
.
The operations record GroupElementOps
contains such functions that can
be used by all types of group elements. Note that there is no element
that has GroupElementsOps
as its operations record. This is
impossible, because there is for example no generic method to multiply or
invert group elements. Thus GroupElementsOps
is only used to inherit
general methods as is done below.
gap> ResidueClassOps := Copy( GroupElementOps );;
Note that the copy is necessary, otherwise the following assignments
would not only change ResidueClassOps
but also GroupElementOps
.
The first function we are implementing is the equality comparison. The
required operation is described simply enough. =
should evaluate to
true
if the operands are equal and false
otherwise. Two residue
classes are of course equal if they have the same representative and the
same modulus. One complication is that when this function is called
either operand may not be a residue class. Of course at least one must
be a residue class otherwise this function would not have been called at
all.
gap> ResidueClassOps.\= := function ( l, r ) > local isEql; > if IsResidueClass( l ) then > if IsResidueClass( r ) then > isEql := l.representative = r.representative > and l.modulus = r.modulus; > else > isEql := false; > fi; > else > if IsResidueClass( r ) then > isEql := false; > else > Error("panic, neither <l> nor <r> is a residue class"); > fi; > fi; > return isEql; > end;;
Note that the quotes around the equal sign =
are necessary, otherwise
it would not be taken as a record component name, as required, but as the
symbol for equality, which must not appear at this place.
Note that we do not have to implement a function for the inequality
operator <
, because it is in the GAP kernel implemented by the
equivalence l < r
is not l = r
.
The next operation is the comparison. We define that one residue class is smaller than another residue class if either it has a smaller modulus or, if the moduli are equal, it has a smaller representative. We must also implement comparisons with other objects.
gap> ResidueClassOps.\< := function ( l, r ) > local isLess; > if IsResidueClass( l ) then > if IsResidueClass( r ) then > isLess := l.representative < r.representative > or (l.representative = r.representative > and l.modulus < r.modulus); > else > isLess := not IsInt( r ) and not IsRat( r ) > and not IsCyc( r ) and not IsPerm( r ) > and not IsWord( r ) and not IsAgWord( r ); > fi; > else > if IsResidueClass( r ) then > isLess := IsInt( l ) or IsRat( l ) > or IsCyc( l ) or IsPerm( l ) > or IsWord( l ) or IsAgWord( l ); > else > Error("panic, neither <l> nor <r> is a residue class"); > fi; > fi; > return isLess; > end;;
The next operation that we must implement is the multiplication *
.
This function is quite complex because it must handle several different
tasks. To make its implementation easier to understand we will start
with a very simple--minded one, which only multiplies residue classes,
and extend it in the following paragraphs.
gap> ResidueClassOps.\* := function ( l, r ) > local prd; # product of l and r, result > if IsResidueClass( l ) then > if IsResidueClass( r ) then > if l.modulus <> r.modulus then > Error("<l> and <r> must have the same modulus"); > fi; > prd := ResidueClass( > l.representative * r.representative, > l.modulus ); > else > Error("product of <l> and <r> must be defined"); > fi; > else > if IsResidueClass( r ) then > Error("product of <l> and <r> must be defined"); > else > Error("panic, neither <l> nor <r> is a residue class"); > fi; > fi; > return prd; > end;;
This function correctly multiplies residue classes, but there are other products that must be implemented. First every group element can be multiplied with a list of group elements, and the result shall be the Operations for Lists). In such a case the above function would only signal an error, which is not acceptable. Therefore we must extend this definition.
gap> ResidueClassOps.\* := function ( l, r ) > local prd; # product of l and r, result > if IsResidueClass( l ) then > if IsResidueClass( r ) then > if l.modulus <> r.modulus then > Error( "<l> and <r> must have the same modulus" ); > fi; > prd := ResidueClass( > l.representative * r.representative, > l.modulus ); > elif IsList( r ) then > prd := List( r, x -> l * x ); > else > Error("product of <l> and <r> must be defined"); > fi; > elif IsList( l ) then > if IsResidueClass( r ) then > prd := List( l, x -> x * r ); > else > Error("panic: neither <l> nor <r> is a residue class"); > fi; > else > if IsResidueClass( r ) then > Error( "product of <l> and <r> must be defined" ); > else > Error("panic, neither <l> nor <r> is a residue class"); > fi; > fi; > return prd; > end;;
This function is almost complete. However it is also allowed to multiply
a group element with a subgroup and the result shall be a coset (see
RightCoset). The operations record of subgroups, which are of course
also represented by records (see Group Records), contains a function
that constructs such a coset. The problem is that in an expression like
subgroup * residue-class
, this function is not called. This is
because the multiplication function in the operations record of the
right operand is called if both operands have such a function (see
Operations for Records). Now in the above case both operands have such
a function. The left operand subgroup has the operations record
GroupOps
(or some refinement thereof), the right operand
residue-class has the operations record ResidueClassOps
. Thus
ResidueClassOps.*
is called. But it does not and also should not know
how to construct a coset. The solution is simple. The multiplication
function for residue classes detects this special case and simply calls
the multiplication function of the left operand.
gap> ResidueClassOps.\* := function ( l, r ) > local prd; # product of l and r, result > if IsResidueClass( l ) then > if IsResidueClass( r ) then > if l.modulus <> r.modulus then > Error( "<l> and <r> must have the same modulus" ); > fi; > prd := ResidueClass( > l.representative * r.representative, > l.modulus ); > elif IsList( r ) then > prd := List( r, x -> l * x ); > else > Error("product of <l> and <r> must be defined"); > fi; > elif IsList( l ) then > if IsResidueClass( r ) then > prd := List( l, x -> x * r ); > else > Error("panic: neither <l> nor <r> is a residue class"); > fi; > else > if IsResidueClass( r ) then > if IsRec( l ) and IsBound( l.operations ) > and IsBound( l.operations.\* ) > and l.operations.\* <> ResidueClassOps.\* > then > prd := l.operations.\*( l, r ); > else > Error("product of <l> and <r> must be defined"); > fi; > else > Error("panic, neither <l> nor <r> is a residue class"); > fi; > fi; > return prd; > end;;
Now we are done with the multiplication.
Next is the powering operation ^
. It is not very complicated. The
PowerMod
function (see PowerMod) does most of what we need,
especially the inversion of elements with the Euclidean algorithm when
the exponent is negative. Note however, that the definition of
operations (see Operations for Group Elements) requires that the
conjugation is available as power of a residue class by another residue
class. This is of course very easy since residue classes form an abelian
group.
gap> ResidueClassOps.\^ := function ( l, r ) > local pow; > if IsResidueClass( l ) then > if IsResidueClass( r ) then > if l.modulus <> r.modulus then > Error("<l> and <r> must have the same modulus"); > fi; > if GcdInt( r.representative, r.modulus ) <> 1 then > Error("<r> must be invertable"); > fi; > pow := l; > elif IsInt( r ) then > pow := ResidueClass( > PowerMod( l.representative, r, l.modulus ), > l.modulus ); > else > Error("power of <l> and <r> must be defined"); > fi; > else > if IsResidueClass( r ) then > Error("power of <l> and <r> must be defined"); > else > Error("panic, neither <l> nor <r> is a residue class"); > fi; > fi; > return pow; > end;;
The last function that we have to write is the printing function. This
is called to print a residue class. It prints the residue class in the
form ResidueClass( representative, modulus )
. It is fairly typical
to print objects in such a form. This form has the advantage that it can
be read back, resulting in exactly the same element, yet it is very
concise.
gap> ResidueClassOps.Print := function ( r ) > Print("ResidueClass( ",r.representative,", ",r.modulus," )"); > end;;
Now we are done with the definition of residue classes as group elements. Try them. We can at this point actually create groups of such elements, and compute in them.
However, we are not yet satisfied. There are two problems with the code we have implemented so far. Different people have different opinions about which of those problems is the graver one, but hopefully all agree that we should try to attack those problems.
The first problem is that it is still possible to define objects via
Group
(see Group) that are not actually groups.
gap> G := Group( ResidueClass(13,43), ResidueClass(13,41) ); Group( ResidueClass( 13, 43 ), ResidueClass( 13, 41 ) )
The other problem is that groups of residue classes constructed with the code we have implemented so far are not handled very efficiently. This is because the generic group algorithms are used, since we have not implemented anything else. For example to test whether a residue class lies in a residue class group, all elements of the residue class group are computed by a Dimino algorithm, and then it is tested whether the residue class is an element of this proper set.
To solve the first problem we must first understand what happens with the
above code if we create a group with Group( res1, res2... )
.
Group
tries to find a domain that contains all the elements res1,
res2, etc. It first calls Domain( [ res1, res2... ] )
(see
Domain). Domain
looks at the residue classes and sees that they all
are records and that they all have a component with the name domain
.
This is understood to be a domain in which the elements lie. And in fact
res1 in GroupElements
is true
, because GroupElements
accepts all
records with tag isGroupElement
. So Domain
returns GroupElements
.
Group
then calls
GroupElements.operations.Group(GroupElements,[res1,res2...],id)
,
where id is the identity residue class, obtained by res1 ^ 0
, and
returns the result.
GroupElementsOps.Group
is the function that actually creates the group.
It does this by simply creating a record with its second argument as
generators list, its third argument as identity, and the generic
GroupOps
as operations record. It ignores the first argument, which is
passed only because convention dictates that a dispatcher passes the
domain as first argument.
So to solve the first problem we must achieve that another function
instead of the generic function GroupElementsOps.Group
is called. This
can be done by persuading Domain
to return a different domain. And
this will happen if the residue classes hold this other domain in their
domain
component.
The obvious choice for such a domain is the (yet to be written) domain
ResidueClasses
. So ResidueClass
must be slightly changed.
gap> ResidueClass := function ( representative, modulus ) > local res; > res := rec(); > res.isGroupElement := true; > res.isResidueClass := true; > res.representative := representative mod modulus; > res.modulus := modulus; > res.domain := ResidueClasses; > res.operations := ResidueClassOps; > return res; > end;;
The main purpose of the domain ResidueClasses
is to construct groups,
so there is very little we have to do. And in fact most of that can be
inherited from GroupElements
.
gap> ResidueClasses := Copy( GroupElements );; gap> ResidueClasses.name := "ResidueClasses";; gap> ResidueClassesOps := Copy( GroupElementsOps );; gap> ResidueClasses.operations := ResidueClassesOps;;
So now we must implement ResidueClassesOps.Group
, which should check
whether the passed elements do in fact form a group. After checking it
simply delegates to the generic function GroupElementsOps.Group
to
create the group as before.
gap> ResidueClassesOps.Group := function ( ResidueClasses, gens, id ) > local g; # one generator from gens > for g in gens do > if g.modulus <> id.modulus then > Error("the generators must all have the same modulus"); > fi; > if GcdInt( g.representative, g.modulus ) <> 1 then > Error("the generators must all be prime residue classes"); > fi; > od; > return GroupElementOps.Group( ResidueClasses, gens, id ); > end;;
This solves the first problem. To solve the second problem, i.e., to
make operations with residue class groups more efficient, we must extend
the function ResidueClassesOps.Group
. It now enters a new operations
record into the group. It also puts the modulus into the group record,
so that it is easier to access.
gap> ResidueClassesOps.Group := function ( ResidueClasses, gens, id ) > local G, # group G, result > gen; # one generator from gens > for gen in gens do > if gen.modulus <> id.modulus then > Error("the generators must all have the same modulus"); > fi; > if GcdInt( gen.representative, gen.modulus ) <> 1 then > Error("the generators must all be prime residue classes"); > fi; > od; > G := GroupElementsOps.Group( ResidueClasses, gens, id ); > G.modulus := id.modulus; > G.operations := ResidueClassGroupOps; > return G; > end;;
Of course now we must build such an operations record. Luckily we do not
have to implement all functions, because we can inherit a lot of
functions from GroupOps
. This is done by copying GroupOps
as we have
done before for ResidueClassOps
and ResidueClassesOps
.
gap> ResidueClassGroupOps := Copy( GroupOps );;
Now the first function that we must write is the Subgroup
function to
ensure that not only groups constructed by Group
have the correct
operations record, but also subgroups of those groups created by
Subgroup
. As in Group
we only check the arguments and then leave the
work to GroupOps.Subgroup
.
gap> ResidueClassGroupOps.Subgroup := function ( G, gens ) > local S, # subgroup of G, result > gen; # one generator from gens > for gen in gens do > if gen.modulus <> G.modulus then > Error("the generators must all have the same modulus"); > fi; > if GcdInt( gen.representative, gen.modulus ) <> 1 then > Error("the generators must all be prime residue classes"); > fi; > od; > S := GroupOps.Subgroup( G, gens ); > S.modulus := G.modulus; > S.operations := ResidueClassGroupOps; > return S; > end;;
The first function that we write especially for residue class groups is
SylowSubgroup
. Since residue class groups are abelian we can compute a
Sylow subgroup of such a group by simply taking appropriate powers of the
generators.
gap> ResidueClassGroupOps.SylowSubgroup := function ( G, p ) > local S, # Sylow subgroup of G, result > gen, # one generator of G > ord, # order of gen > gens; # generators of S > gens := []; > for gen in G.generators do > ord := OrderMod( gen.representative, G.modulus ); > while ord mod p = 0 do ord := ord / p; od; > Add( gens, gen ^ ord ); > od; > S := Subgroup( Parent( G ), gens ); > return S; > end;;
To allow the other functions that are applicable to residue class groups to work efficiently we now want to make use of the fact that residue class groups are direct products of cyclic groups and that we know what those factors are and how we can project onto those factors.
To do this we write ResidueClassGroupOps.MakeFactors
that adds the
components facts
, roots
, sizes
, and sgs
to the group record G.
This information, detailed below, will enable other functions to work
efficiently with such groups. Creating such information is a fairly
typical thing, for example for permutation groups the corresponding
information is the stabilizer chain computed by MakeStabChain
.
G.facts
will be the list of prime power factors of G.modulus
.
Actually this is a little bit more complicated, because the residue class
group modulo the largest power q of 2 that divides G.modulus
need
not be cyclic. So if q is a multiple of 4, G.facts[1]
will be 4,
corresponding to the projection of G into (Z / 4 Z)^* (of size 2),
furthermore if q is a multiple of 8, G.facts[2]
will be q,
corresponding to the projection of G into the subgroup generated by 5
in (Z / q Z)^* (of size q/4).
G.roots
will be a list of primitive roots, i.e., of generators of the
corresponding factors in G.facts
. G.sizes
will be a list of the
sizes of the corresponding factors in G.facts
, i.e., G.sizes[i]
= Phi( G.facts[i] )
. (If G.modulus
is a multiple of 8,
G.roots[2]
will be 5, and G.sizes[2]
will be q/4.)
Now we can represent each element g of the group G by a list e,
called the exponent vector, of the length of G.facts
, where
e[i]
is the logarithm of g.representative mod G.facts[i]
with respect to G.roots[i]
. The multiplication of elements of G
corresponds to the componentwise addition of their exponent vectors,
where we add modulo G.sizes[i]
in the i-th component. (Again
special consideration are necessary if G.modulus
is divisible by 8.)
Next we compute the exponent vectors of all generators of G, and
represent this information as a matrix. Then we bring this matrix into
upper triangular form, with an algorithm that is very much like the
ordinary Gaussian elimination, modified to account for the different
sizes of the components. This upper triangular matrix of exponent
vectors is the component G.sgs
. This new matrix obviously still
contains the exponent vectors of a generating system of G, but a much
nicer one, which allows us to tackle problems one component at a time.
(It is not necessary that you fully check this, the important thing here
is not the mathematical side.)
gap> ResidueClassGroupOps.MakeFactors := function ( G ) > local p, q, # prime factor of modulus and largest power > r, s, # two rows of the standard generating system > g, # extended gcd of leading entries in r, s > x, y, # two entries in r and s > i, k, l; # loop variables > > # find the factors of the direct product > G.facts := []; > G.roots := []; > G.sizes := []; > for p in Set( Factors( G.modulus ) ) do > q := p; > while G.modulus mod (p*q) = 0 do q := p*q; od; > if q mod 4 = 0 then > Add( G.facts, 4 ); > Add( G.roots, 3 ); > Add( G.sizes, 2 ); > fi; > if q mod 8 = 0 then > Add( G.facts, q ); > Add( G.roots, 5 ); > Add( G.sizes, q/4 ); > fi; > if p <> 2 then > Add( G.facts, q ); > Add( G.roots, PrimitiveRootMod( q ) ); > Add( G.sizes, (p-1)*q/p ); > fi; > od; > > # represent each generator in this factorization > G.sgs := []; > for k in [ 1 .. Length( G.generators ) ] do > G.sgs[k] := []; > for i in [ 1 .. Length( G.facts ) ] do > if G.facts[i] mod 8 = 0 then > if G.generators[k].representative mod 4 = 1 then > G.sgs[k][i] := LogMod( > G.generators[k].representative, > G.roots[i], G.facts[i] ); > else > G.sgs[k][i] := LogMod( > -G.generators[k].representative, > G.roots[i], G.facts[i] ); > fi; > else > G.sgs[k][i] := LogMod( > G.generators[k].representative, > G.roots[i], G.facts[i] ); > fi; > od; > od; > for i in [ Length( G.sgs ) + 1 .. Length( G.facts ) ] do > G.sgs[i] := 0 * G.facts; > od; > > # bring this matrix to diagonal form > for i in [ 1 .. Length( G.facts ) ] do > r := G.sgs[i]; > for k in [ i+1 .. Length( G.sgs ) ] do > s := G.sgs[k]; > g := Gcdex( r[i], s[i] ); > for l in [ i .. Length( r ) ] do > x := r[l]; y := s[l]; > r[l] := (g.coeff1 * x + g.coeff2 * y) mod G.sizes[l]; > s[l] := (g.coeff3 * x + g.coeff4 * y) mod G.sizes[l]; > od; > od; > s := []; > x := G.sizes[i] / GcdInt( G.sizes[i], r[i] ); > for l in [ 1 .. Length( r ) ] do > s[l] := (x * r[l]) mod G.sizes[l]; > od; > Add( G.sgs, s ); > od; > > end;;
With the information computed by MakeFactors
it is now of course very
easy to compute the size of a residue class group. We just look at the
G.sgs
, and multiply the orders of the leading exponents of the
nonzero exponent vectors.
gap> ResidueClassGroupOps.Size := function ( G ) > local s, # size of G, result > i; # loop variable > if not IsBound( G.facts ) then > G.operations.MakeFactors( G ); > fi; > s := 1; > for i in [ 1 .. Length( G.facts ) ] do > s := s * G.sizes[i] / GcdInt( G.sizes[i], G.sgs[i][i] ); > od; > return s; > end;;
The membership test is a little bit more complicated. First we test that
the first argument is really a residue class with the correct modulus.
Then we compute the exponent vector of this residue class and reduce this
exponent vector using the upper triangular matrix G.sgs
.
gap> ResidueClassGroupOps.\in := function ( res, G ) > local s, # exponent vector of res > g, # extended gcd > x, y, # two entries in s and G.sgs[i] > i, l; # loop variables > if not IsResidueClass( res ) > or res.modulus <> G.modulus > or GcdInt( res.representative, res.modulus ) <> 1 > then > return false; > fi; > if not IsBound( G.facts ) then > G.operations.MakeFactors( G ); > fi; > s := []; > for i in [ 1 .. Length( G.facts ) ] do > if G.facts[i] mod 8 = 0 then > if res.representative mod 4 = 1 then > s[i] := LogMod( res.representative, > G.roots[i], G.facts[i] ); > else > s[i] := LogMod( -res.representative, > G.roots[i], G.facts[i] ); > fi; > else > s[i] := LogMod( res.representative, > G.roots[i], G.facts[i] ); > fi; > od; > for i in [ 1 .. Length( G.facts ) ] do > if s[i] mod GcdInt( G.sizes[i], G.sgs[i][i] ) <> 0 then > return false; > fi; > g := Gcdex( s[i], G.sgs[i][i] ); > for l in [ i .. Length( G.facts ) ] do > x := s[l]; y := G.sgs[i][l]; > s[l] := (g.coeff3 * x + g.coeff4 * y) mod G.sizes[l]; > od; > od; > return true; > end;;
We also add a function Random
that works by creating a random exponent
as a random linear combination of the exponent vectors in G.sgs
, and
converts this exponent vector to a residue class. (The main purpose of
this function is to allow you to create random test examples for the
other functions.)
gap> ResidueClassGroupOps.Random := function ( G ) > local s, # exponent vector of random element > r, # vector of remainders in each factor > i, k, l; # loop variables > if not IsBound( G.facts ) then > G.operations.MakeFactors( G ); > fi; > s := 0 * G.facts; > for i in [ 1 .. Length( G.facts ) ] do > l := G.sizes[i] / GcdInt( G.sizes[i], G.sgs[i][i] ); > k := Random( [ 0 .. l-1 ] ); > for l in [ i .. Length( s ) ] do > s[l] := (s[l] + k * G.sgs[i][l]) mod G.sizes[l]; > od; > od; > r := []; > for l in [ 1 .. Length( s ) ] do > r[l] := PowerModInt( G.roots[l], s[l], G.facts[l] ); > if G.facts[l] mod 8 = 0 and r[1] = 3 then > r[l] := G.facts[l] - r[l]; > fi; > od; > return ResidueClass( ChineseRem( G.facts, r ), G.modulus ); > end;;
There are a lot more functions that would benefit from being implemented especially for residue class groups. We do not show them here, because the above functions already displayed how such functions can be written.
To round things up, we finally add a function that constructs the full
residue class group given a modulus m. This function is totally
independent of the implementation of residue classes and residue class
groups. It only has to find a (minimal) system of generators of the full
prime residue classes group, and to call Group
to construct this group.
It also adds the information entry size
to the group record, of course
with the value phi(n).
gap> PrimeResidueClassGroup := function ( m ) > local G, # group Z/mZ, result > gens, # generators of G > p, q, # prime and prime power dividing m > r, # primitive root modulo q > g; # is = r mod q and = 1 mod m/q > > # add generators for each prime power factor q of m > gens := []; > for p in Set( Factors( m ) ) do > q := p; > while m mod (q * p) = 0 do q := q * p; od; > > # (Z/4Z)^* = < 3 > > if q = 4 then > r := 3; > g := r + q * (((1/q mod (m/q)) * (1 - r)) mod (m/q)); > Add( gens, ResidueClass( g, m ) ); > > # (Z/8nZ)^* = < 5, -1 > is not cyclic > elif q mod 8 = 0 then > r := q-1; > g := r + q * (((1/q mod (m/q)) * (1 - r)) mod (m/q)); > Add( gens, ResidueClass( g, m ) ); > r := 5; > g := r + q * (((1/q mod (m/q)) * (1 - r)) mod (m/q)); > Add( gens, ResidueClass( g, m ) ); > > # for odd q, (Z/qZ)^* is cyclic > elif q <> 2 then > r := PrimitiveRootMod( q ); > g := r + q * (((1/q mod (m/q)) * (1 - r)) mod (m/q)); > Add( gens, ResidueClass( g, m ) ); > fi; > > od; > > # return the group generated by gens > G := Group( gens, ResidueClass( 1, m ) ); > G.size := Phi( n ); > return G; > end;;
There is one more thing that we can learn from this example. Mathematically a residue class is not only a group element, but a set as well. We can reflect this in GAP by turning residue classes into domains (see Domains). Section About Defining New Domains gives an example of how to implement a new domain, so we will here only show the code with few comments.
First we must change the function that constructs a residue class, so that it enters the necessary fields to tag this record as a domain. It also adds the information that residue classes are infinite.
gap> ResidueClass := function ( representative, modulus ) > local res; > res := rec(); > res.isGroupElement := true; > res.isDomain := true; > res.isResidueClass := true; > res.representative := representative mod modulus; > res.modulus := modulus; > res.isFinite := false; > res.size := "infinity"; > res.domain := ResidueClasses; > res.operations := ResidueClassOps; > return res; > end;;
The initialization of the ResidueClassOps
record must be changed too,
because now we want to inherit both from GroupElementsOps
and
DomainOps
. This is done by the function MergedRecord
, which takes
two records and returns a new record that contains all components from
either record.
Note that the record returned by MergedRecord
does not have those
components that appear in both arguments. This forces us to explicitly
write down from which record we want to inherit those functions, or to
define them anew. In our example the components common to
GroupElementOps
and DomainOps
are only the equality and ordering
functions, which we have to define anyhow. (This solution for the
problem of which definition to choose in the case of multiple inheritance
is also taken by C++.)
With this function definition we can now initialize ResidueClassOps
.
gap> ResidueClassOps := MergedRecord( GroupElementOps, DomainOps );;
Now we add all functions to this record as described above.
Next we add a function to the operations record that tests whether a certain object is in a residue class.
gap> ResidueClassOps.\in := function ( element, class ) > if IsInt( element ) then > return (element mod class.modulus = class.representative); > else > return false; > fi; > end;;
Finally we add a function to compute the intersection of two residue classes.
gap> ResidueClassOps.Intersection := function ( R, S ) > local I, # intersection of R and S, result > gcd; # gcd of the moduli > if IsResidueClass( R ) then > if IsResidueClass( S ) then > gcd := GcdInt( R.modulus, S.modulus ); > if R.representative mod gcd > <> S.representative mod gcd > then > I := []; > else > I := ResidueClass( > ChineseRem( > [ R.modulus, S.modulus ] , > [ R.representative, S.representative ]), > Lcm( R.modulus, S.modulus ) ); > fi; > else > I := DomainOps.Intersection( R, S ); > fi; > else > I := DomainOps.Intersection( R, S ); > fi; > return I; > end;;
There is one further thing that we have to do. When Group
is called
with a single argument that is a domain, it assumes that you want to
create a new group such that there is a bijection between the original
domain and the new group. This is not what we want here. We want that
in this case we get the cyclic group that is generated by the single
residue class. (This overloading of Group
is probably a mistake, but
so is the overloading of residue classes, which are both group elements
and domains.) The following definition solves this problem.
gap> ResidueClassOps.Group := function ( R ) > return ResidueClassesOps.Group( ResidueClasses, [R], R^0 ); > end;;
This concludes our example. There are however several further things
that you could do. One is to add functions for the quotient, the
modulus, etc. Another is to fix the functions so that they do not hang
if asked for the residue class group mod 1. Also you might try to
implement residue class rings analogous to residue class groups. Finally
it might be worthwhile to improve the speed of the multiplication of
prime residue classes. This can be done by doing some precomputation in
ResidueClass
and adding some information to the residue class record
for prime residue classes (Mon85).
GAP 3.4.4