In this section we will show how one can add a new domain to GAP. All domains are implemented in the library in this way. We will use the ring of Gaussian integers as our example.
Note that everything defined here is already in the library file
LIBNAME/"gaussian.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.
The elements of this domain are already available, because Gaussian
integers are just a special case of cyclotomic numbers. As is described
in chapter Cyclotomics E(4)
is GAP's name for the complex root of
-1. So all Gaussian integers can be represented as a + b*E(4)
,
where a and b are ordinary integers.
As was already mentioned each domain is represented by a record. So we
create a record to represent the Gaussian integers, which we call
GaussianIntegers
.
gap> GaussianIntegers := rec();;
The first components that this record must have are those that identify this record as a record denoting a ring domain. Those components are called the category components.
gap> GaussianIntegers.isDomain := true;; gap> GaussianIntegers.isRing := true;;
The next components are those that uniquely identify this ring. For
rings this must be generators
, zero
, and one
. Those components are
called the identification components of the domain record. We also
assign a name component. This name will be printed when the domain is
printed.
gap> GaussianIntegers.generators := [ 1, E(4) ];; gap> GaussianIntegers.zero := 0;; gap> GaussianIntegers.one := 1;; gap> GaussianIntegers.name := "GaussianIntegers";;
Next we enter some components that represent knowledge that we have about
this domain. Those components are called the knowledge components. In
our example we know that the Gaussian integers form a infinite,
commutative, integral, Euclidean ring, which has an unique factorization
property, with the four units 1, -1, E(4)
, and -E(4)
.
gap> GaussianIntegers.size := "infinity";; gap> GaussianIntegers.isFinite := false;; gap> GaussianIntegers.isCommutativeRing := true;; gap> GaussianIntegers.isIntegralRing := true;; gap> GaussianIntegers.isUniqueFactorizationRing := true;; gap> GaussianIntegers.isEuclideanRing := true;; gap> GaussianIntegers.units := [1,-1,E(4),-E(4)];;
This was the easy part of this example. Now we have to add an
operations record to the domain record. This operations record
(GaussianIntegers.operations
) shall contain functions that implement
all the functions mentioned in chapter Rings, e.g., DefaultRing
,
IsCommutativeRing
, Gcd
, or QuotientRemainder
.
Luckily we do not have to implement all this functions. The first class
of functions that we need not implement are those that can simply get the
result from the knowledge components. E.g., IsCommutativeRing
looks
for the knowledge component isCommutativeRing
, finds it and returns
this value. So GaussianIntegers.operations.IsCommutativeRing
is never
called.
gap> IsCommutativeRing( GaussianIntegers ); true gap> Units( GaussianIntegers ); [ 1, -1, E(4), -E(4) ]
The second class of functions that we need not implement are those for which there is a general algorithm that can be applied for all rings. For example once we can do a division with remainder (which we will have to implement) we can use the general Euclidean algorithm to compute the greatest common divisor of elements.
So the question is, how do we get those general functions into our
operations record. This is very simple, we just initialize the
operations record as a copy of the record RingOps
, which contains all
those general functions. We say that GaussianIntegers.operations
inherits the general functions from RingOps
.
gap> GaussianIntegersOps := OperationsRecord( > "GaussianIntegersOps", RingOps );; gap> GaussianIntegers.operations := GaussianIntegersOps;;
So now we have to add those functions whose result can not (easily) be
derived from the knowledge components and that we can not inherit from
RingOps
.
The first such function is the membership test. This function must test
whether an object is an element of the domain GaussianIntegers
.
IsCycInt(x)
tests whether x is a cyclotomic integer and
NofCyc(x)
returns the smallest n such that the cyclotomic x can
be written as a linear combination of powers of the primitive n-th root
of unity E(n)
. If NofCyc(x)
returns 1, x is an ordinary
rational number.
gap> GaussianIntegersOps.\in := function ( x, GaussInt ) > return IsCycInt( x ) and (NofCyc( x ) = 1 or NofCyc( x ) = 4); > end;;
Note that the second argument GaussInt
is not used in the function.
Whenever this function is called, the second argument must be
GaussianIntegers
, because GaussianIntegers
is the only domain that
has this particular function in its operations record. This also happens
for most other functions that we will write. This argument can not be
dropped though, because there are other domains that share a common in
function, for example all permutation groups have the same in
function.
If the operator in
would not pass the second argument, this function
could not know for which permutation group it should perform the
membership test.
So now we can test whether a certain object is a Gaussian integer or not.
gap> E(4) in GaussianIntegers; true gap> 1/2 in GaussianIntegers; false gap> GaussianIntegers in GaussianIntegers; false
Another function that is just as easy is the function Random
that
should return a random Gaussian integer.
gap> GaussianIntegersOps.Random := function ( GaussInt ) > return Random( Integers ) + Random( Integers ) * E( 4 ); > end;;
Note that actually a Random
function was inherited from RingOps
. But
this function can not be used. It tries to construct the sorted list of
all elements of the domain and then picks a random element from that
list. Therefor this function is only applicable for finite domains, and
can not be used for GaussianIntegers
. So we overlay this default
function by simply putting another function in the operations record.
Now we can already test whether a Gaussian integer is a unit or not.
This is because the default function inherited from RingOps
tests
whether the knowledge component units
is present, and it returns true
if the element is in that list and false
otherwise.
gap> IsUnit( GaussianIntegers, E(4) ); true gap> IsUnit( GaussianIntegers, 1 + E(4) ); false
Now we finally come to more interesting stuff. The function Quotient
should return the quotient of its two arguments x and y. If the
quotient does not exist in the ring (i.e., if it is a proper Gaussian
rational), it must return false
. (Without this last requirement we
could do without the Quotient
function and always simply use the /
operator.)
gap> GaussianIntegersOps.Quotient := function ( GaussInt, x, y ) > local q; > q := x / y; > if not IsCycInt( q ) then > q := false; > fi; > return q; > end;;
The next function is used to test if two elements are associate in the
ring of Gaussian integers. In fact we need not implement this because
the function that we inherit from RingOps
will do fine. The following
function is a little bit faster though that the inherited one.
gap> GaussianIntegersOps.IsAssociated := function ( GaussInt, x, y ) > return x = y or x = -y or x = E(4)*y or x = -E(4)*y; > end;;
We must however implement the function StandardAssociate
. It should
return an associate that is in some way standard. That means, whenever
we apply StandardAssociate
to two associated elements we must obtain
the same value. For Gaussian integers we return that associate that lies
in the first quadrant of the complex plane. That is, the result is that
associated element that has positive real part and nonnegative imaginary
part. 0 is its own standard associate of course. Note that this is a
generalization of the absolute value function, which is
StandardAssociate
for the integers. The reason that we must implement
StandardAssociate
is of course that there is no general way to compute
a standard associate for an arbitrary ring, there is not even a standard
way to define this!
gap> GaussianIntegersOps.StandardAssociate := function ( GaussInt, x ) > if IsRat(x) and 0 <= x then > return x; > elif IsRat(x) then > return -x; > elif 0 < COEFFSCYC(x)[1] and 0 <= COEFFSCYC(x)[2] then > return x; > elif COEFFSCYC(x)[1] <= 0 and 0 < COEFFSCYC(x)[2] then > return - E(4) * x; > elif COEFFSCYC(x)[1] < 0 and COEFFSCYC(x)[2] <= 0 then > return - x; > else > return E(4) * x; > fi; > end;;
Note that COEFFSCYC
is an internal function that returns the
coefficients of a Gaussian integer (actually of an arbitrary cyclotomic)
as a list.
Now we have implemented all functions that are necessary to view the Gaussian integers plainly as a ring. Of course there is not much we can do with such a plain ring, we can compute with its elements and can do a few things that are related to the group of units.
gap> Quotient( GaussianIntegers, 2, 1+E(4) ); 1-E(4) gap> Quotient( GaussianIntegers, 3, 1+E(4) ); false gap> IsAssociated( GaussianIntegers, 1+E(4), 1-E(4) ); true gap> StandardAssociate( GaussianIntegers, 3 - E(4) ); 1+3*E(4)
The remaining functions are related to the fact that the Gaussian integers are an Euclidean ring (and thus also a unique factorization ring).
The first such function is EuclideanDegree
. In our example the
Euclidean degree of a Gaussian integer is of course simply its norm.
Just as with StandardAssociate
we must implement this function because
there is no general way to compute the Euclidean degree for an arbitrary
Euclidean ring. The function itself is again very simple. The Euclidean
degree of a Gaussian integer x is the product of x with its complex
conjugate, which is denoted in GAP by GaloisCyc( x, -1 )
.
gap> GaussianIntegersOps.EuclideanDegree := function ( GaussInt, x ) > return x * GaloisCyc( x, -1 ); > end;;
Once we have defined the Euclidean degree we want to implement the
QuotientRemainder
function that gives us the Euclidean quotient and
remainder of a division.
gap> GaussianIntegersOps.QuotientRemainder := function ( GaussInt, x, y ) > return [ RoundCyc( x/y ), x - RoundCyc( x/y ) * y ]; > end;;
Note that in the definition of QuotientRemainder
we must use the
function RoundCyc
, which views the Gaussian rational x/y
as a
point in the complex plane and returns the point of the lattice spanned
by 1 and E(4)
closest to the point x/y
. If we would truncate
towards the origin instead (this is done by the function IntCyc
) we
could not guarantee that the result of EuclideanRemainder
always has
Euclidean degree less than the Euclidean degree of y as the following
example shows.
gap> x := 2 - E(4);; EuclideanDegree( GaussianIntegers, x ); 5 gap> y := 2 + E(4);; EuclideanDegree( GaussianIntegers, y ); 5 gap> q := x / y; q := IntCyc( q ); 3/5-4/5*E(4) 0 gap> EuclideanDegree( GaussianIntegers, x - q * y ); 5
Now that we have implemented the QuotientRemainder
function we can
compute greatest common divisors in the ring of Gaussian integers. This
is because we have inherited from RingOps
the general function Gcd
that computes the greatest common divisor using Euclid's algorithm,
which only uses QuotientRemainder
(and StandardAssociate
to return
the result in a normal form). Of course we can now also compute least
common multiples, because that only uses Gcd
.
gap> Gcd( GaussianIntegers, 2, 5 - E(4) ); 1+E(4) gap> Lcm( GaussianIntegers, 2, 5 - E(4) ); 6+4*E(4)
Since the Gaussian integers are a Euclidean ring they are also a unique factorization ring. The next two functions implement the necessary operations. The first is the test for primality. A rational integer is a prime in the ring of Gaussian integers if and only if it is congruent to 3 modulo 4 (the other rational integer primes split into two irreducibles), and a Gaussian integer that is not a rational integer is a prime if its norm is a rational integer prime.
gap> GaussianIntegersOps.IsPrime := function ( GaussInt, x ) > if IsInt( x ) then > return x mod 4 = 3 and IsPrimeInt( x ); > else > return IsPrimeInt( x * GaloisCyc( x, -1 ) ); > fi; > end;;
The factorization is based on the same observation. We compute the
Euclidean degree of the number that we want to factor, and factor this
rational integer. Then for every rational integer prime that is
congruent to 3 modulo 4 we get one factor, and we split the other
rational integer primes using the function TwoSquares
and test which
irreducible divides.
gap> GaussianIntegersOps.Factors := function ( GaussInt, x ) > local facs, # factors (result) > prm, # prime factors of the norm > tsq; # representation of prm as x^2 + y^2 > > # handle trivial cases > if x in [ 0, 1, -1, E(4), -E(4) ] then > return [ x ]; > fi; > > # loop over all factors of the norm of x > facs := []; > for prm in Set( FactorsInt( EuclideanDegree( x ) ) ) do > > # p = 2 and primes p = 1 mod 4 split according to p = x^2+y^2 > if prm = 2 or prm mod 4 = 1 then > tsq := TwoSquares( prm ); > while IsCycInt( x / (tsq[1]+tsq[2]*E(4)) ) do > Add( facs, (tsq[1]+tsq[2]*E(4)) ); > x := x / (tsq[1]+tsq[2]*E(4)); > od; > while IsCycInt( x / (tsq[2]+tsq[1]*E(4)) ) do > Add( facs, (tsq[2]+tsq[1]*E(4)) ); > x := x / (tsq[2]+tsq[1]*E(4)); > od; > > # primes p = 3 mod 4 stay prime > else > while IsCycInt( x / prm ) do > Add( facs, prm ); > x := x / prm; > od; > fi; > > od; > > # the first factor takes the unit > facs[1] := x * facs[1]; > > # return the result > return facs; > end;;
So now we can factorize numbers in the ring of Gaussian integers.
gap> Factors( GaussianIntegers, 10 ); [ -1-E(4), 1+E(4), 1+2*E(4), 2+E(4) ] gap> Factors( GaussianIntegers, 103 ); [ 103 ]
Now we have written all the functions for the operations record that
implement the operations. We would like one more thing however. Namely
that we can simply write Gcd( 2, 5 - E(4) )
without having to specify
GaussianIntegers
as first argument. Gcd
and the other functions
should be clever enough to find out that the arguments are Gaussian
integers and call GaussianIntegers.operations.Gcd
automatically.
To do this we must first understand what happens when Gcd
is called
without a ring as first argument. For an example suppose that we have
called Gcd( 66, 123 )
(and want to compute the gcd over the integers).
First Gcd
calls DefaultRing( [ 66, 123 ] )
, to obtain a ring that
contains 66 and 123. DefaultRing
then calls Domain( [ 66, 123 ] )
to
obtain a domain, which need not be a ring, that contains 66 and 123.
Domain
is the only function in the whole GAP library that knows
about the various types of elements. So it looks at its argument and
decides to return the domain Integers
(which is in fact already a ring,
but it could in principle also return Rationals
). DefaultRing
now
calls Integers.operations.DefaultRing( [ 66, 123 ] )
and expects a ring
in which the requested gcd computation can be performed.
Integers.operations.DefaultRing( [ 66, 123 ] )
also returns Integers
.
So DefaultRing
returns Integers
to Gcd
and Gcd
finally calls
Integers.operations.Gcd( Integers, 66, 123 )
.
So the first thing we must do is to tell Domain
about Gaussian
integers. We do this by extending Domain
with the two lines
elif ForAll( elms, IsGaussInt ) then return GaussianIntegers;
so that it now looks as follows.
gap> Domain := function ( elms ) > local elm; > if ForAll( elms, IsInt ) then > return Integers; > elif ForAll( elms, IsRat ) then > return Rationals; > elif ForAll( elms, IsFFE ) then > return FiniteFieldElements; > elif ForAll( elms, IsPerm ) then > return Permutations; > elif ForAll( elms, IsMat ) then > return Matrices; > elif ForAll( elms, IsWord ) then > return Words; > elif ForAll( elms, IsAgWord ) then > return AgWords; > elif ForAll( elms, IsGaussInt ) then > return GaussianIntegers; > elif ForAll( elms, IsCyc ) then > return Cyclotomics; > else > for elm in elms do > if IsRec(elm) and IsBound(elm.domain) > and ForAll( elms, l -> l in elm.domain ) > then > return elm.domain; > fi; > od; > Error("sorry, the elements lie in no common domain"); > fi; > end;;
Of course we must define a function IsGaussInt
, otherwise this could
not possibly work. This function is similar to the membership test we
already defined above.
gap> IsGaussInt := function ( x ) > return IsCycInt( x ) and (NofCyc( x ) = 1 or NofCyc( x ) = 4); > end;;
Then we must define a function DefaultRing
for the Gaussian integers
that does nothing but return GaussianIntegers
.
gap> GaussianIntegersOps.DefaultRing := function ( elms ) > return GaussianIntegers; > end;;
Now we can call Gcd
with two Gaussian integers without having to pass
GaussianIntegers
as first argument.
gap> Gcd( 2, 5 - E(4) ); 1+E(4)
Of course GAP can not read your mind. In the following example it
assumes that you want to factor 10 over the ring of integers, not over
the ring of Gaussian integers (because Integers
is the default ring
containing 10). So if you want to factor a rational integer over the
ring of Gaussian integers you must pass GaussianIntegers
as first
argument.
gap> Factors( 10 ); [ 2, 5 ] gap> Factors( GaussianIntegers, 10 ); [ -1-E(4), 1+E(4), 1+2*E(4), 2+E(4) ]
This concludes our example. In the file LIBNAME/"gaussian.g"
you
will also find the definition of the field of Gaussian rationals. It is
so similar to the above definition that there is no point in discussing
it here. The next section shows you what further considerations are
necessary when implementing a type of parametrized domains (demonstrated
by implementing full symmetric permutation groups). For further details
see chapter Gaussians for a description of the Gaussian integers and
rationals and chapter Rings for a list of all functions applicable to
rings.
GAP 3.4.4