In this section we will open the black boxes and describe how all this works. This is complex and you do not need to understand it if you are content to use domains only as black boxes. So you may want to skip this section (and the remainder of this chapter).
Domains are represented by records, which we will call domain records in the following. Which components have to be present, which may, and what those components hold, differs from category to category, and, to a smaller extent, from domain to domain. It is possible, though, to generally distinguish four types of components.
The first type of components are called the category components. They
determine to which category a domain belongs. A domain D in a category
Cat has a component isCat
with the value true
. For example, each
group has the component isGroup
. Also each domain has the component
isDomain
(again with the value true
). Finally a domain may also have
components that describe the representation of this domain. For example,
each permutation group has a component isPermGroup
(again with the
value true
). Functions such as IsPermGroup
test whether such a
component is present, and whether it has the value true
.
The second type of components are called the identification components.
They distinguish the domain from other domains in the same category. The
identification components uniquely identify the domain. For example, for
groups the identification components are generators
, which holds a list
of generators of the group, and identity
, which holds the identity of
the group (needed for the trivial group, for which the list of generators
is empty).
The third type of components are called knowledge components. They
hold all the knowledge GAP has about the domain. For example the size
of the domain D is stored in the knowledge component D.size
, the
commutator subgroup of a group is stored in the knowledge component
D.commutatorSubgroup
, etc. Of course, the knowledge about a certain
domain will usually increase as you work with a domain. For example, a
group record may initially hold only the knowledge that the group is
finite, but may later hold all kinds of knowledge, for example the
derived series, the Sylow subgroups, etc.
Finally each domain record contains an operations record. The operations record is discussed below.
We want to emphasize that really all information that GAP has about a domain is stored in the knowledge components. That means that you can access all this information, at least if you know where to look and how to interpret what you see. The chapters describing categories and domains will tell you what knowledge components a domain may have, and how the knowledge is represented in those components.
For an example let us return to the permutation group a5
from section
About Domains and Categories. If we print the record using the
function PrintRec
we see all the information. GAP stores the
stabilizer chain of a5
in the components orbit
, transversal
, and
stabilizer
. It is not important that you understand what a stabilizer
chain is (this is discussed in chapter Permutation Groups), the
important point here is that it is the vital information that GAP
needs to work efficiently with a5
and that you can access it.
gap> a5 := Group( (1,2,3), (3,4,5) ); Group( (1,2,3), (3,4,5) ) gap> Size( a5 ); 60 gap> PrintRec( a5 ); Print( "\n" ); rec( isDomain := true, isGroup := true, identity := (), generators := [ (1,2,3), (3,4,5) ], operations := ..., isPermGroup := true, isFinite := true, 1 := (1,2,3), 2 := (3,4,5), orbit := [ 1, 3, 2, 5, 4 ], transversal := [ (), (1,2,3), (1,2,3), (3,4,5), (3,4,5) ], stabilizer := rec( identity := (), generators := [ (3,4,5), (2,5,3) ], orbit := [ 2, 3, 5, 4 ], transversal := [ , (), (2,5,3), (3,4,5), (3,4,5) ], stabilizer := rec( identity := (), generators := [ (3,4,5) ], orbit := [ 3, 5, 4 ], transversal := [ ,, (), (3,4,5), (3,4,5) ], stabilizer := rec( identity := (), generators := [ ], operations := ... ), operations := ... ), operations := ... ), isParent := true, stabChainOptions := rec( random := 1000, operations := ... ), stabChain := rec( generators := [ (1,2,3), (3,4,5) ], identity := (), orbit := [ 1, 3, 2, 5, 4 ], transversal := [ (), (1,2,3), (1,2,3), (3,4,5), (3,4,5) ], stabilizer := rec( identity := (), generators := [ (3,4,5), (2,5,3) ], orbit := [ 2, 3, 5, 4 ], transversal := [ , (), (2,5,3), (3,4,5), (3,4,5) ], stabilizer := rec( identity := (), generators := [ (3,4,5) ], orbit := [ 3, 5, 4 ], transversal := [ ,, (), (3,4,5), (3,4,5) ], stabilizer := rec( identity := (), generators := [ ], operations := ... ), operations := ... ), operations := ... ), operations := ... ), size := 60 )
Note that you can not only read this information, you can also modify it. However, unless you truly understand what you are doing, we discourage you from playing around. All GAP functions assume that the information in the domain record is in a consistent state, and everything will go wrong if it is not.
gap> a5.size := 120; 120 gap> Size( ConjugacyClass( a5, (1,2,3,4,5) ) ); 24 # this is of course wrong
As was mentioned above, each domain record has an operations record. We
have already seen that functions such as Size
can be applied to various
types of domains. It is clear that there is no general method that will
compute the size of all domains efficiently. So Size
must somehow
decide which method to apply to a given domain. The operations record
makes this possible.
The operations record of a domain D is the component with the name
D.operations
, its value is a record. For each function that you can
apply to D this record contains a function that will compute the
required information (hopefully in an efficient way).
To understand this let us take a look at what happens when we compute
Size( a5 )
. Not much happens. Size
simply calls
a5.operations.Size( a5 )
. a5.operations.Size
is a function written
especially for permutation groups. It computes the size of a5
and
returns it. Then Size
returns this value.
Actually Size
does a little bit more than that. It first tests whether
a5
has the knowledge component a5.size
. If this is the case, Size
simply returns that value. Otherwise it calls a5.operations.Size( a5 )
to compute the size. Size
remembers the result in the knowledge
component a5.size
so that it is readily available the next time Size(
a5 )
is called. The complete definition of Size
is as follows.
gap> Size := function ( D ) > local size; > if IsSet( D ) then > size := Length( D ); > elif IsRec( D ) and IsBound( D.size ) then > size := D.size; > elif IsDomain( D ) then > D.size := D.operations.Size( D ); > size := D.size; > else > Error( "<D> must be a domain or a set" ); > fi; > return size; > end;;
Because functions such as Size
only dispatch to the functions in the
operations record, they are called dispatcher functions. Almost all
functions that you call directly are dispatcher functions, and almost all
functions that do the hard work are components in an operations record.
Which function is called by a dispatcher obviously depends on the domain
and its operations record (that is the whole point of having an
operations record). In principle each domain could have its own Size
function. In practice however, this would require too many functions.
So different domains share the functions in their operations records,
usually all domains with the same representation share all their
operations record functions. For example all permutation groups share
the same Size
function. Because this shared Size
function must be
able to access the information in the domain record to compute the
correct result, the Size
dispatcher function (and all other dispatchers
as well) pass the domain as first argument
In fact the domains not only have the same functions in their operations
record, they share the operations record. So for example all permutation
groups share a common operations record, which is called PermGroupOps
.
This means that changing a function in the operations record for a domain
D in the following way D.operations.function := new-function;
will also change this function for all domains of the same type, even
those that do not yet exist at the moment of the assignment and will only
be constructed later. This is usually not desirable, since supposedly
new-function uses some special properties of the domain D to work
more efficiently. We suggest therefore that you first make a copy of the
operations record with D.operations := Copy( D.operations );
and
only afterwards do D.operations.function := new-function;
.
If a programmer that implements a new domain D, a new type of groups
say, would have to write all functions applicable to D, this would
require a lot of effort. For example, there are about 120 functions
applicable to groups. Luckily many of those functions are independent of
the particular type of groups. For example the following function will
compute the commutator subgroup of any group, assuming that
TrivialSubgroup
, Closure
, and NormalClosure
work. We say that this
function is generic.
gap> GroupOps.CommutatorSubgroup := function ( U, V ) > local C, u, v, c; > C := TrivialSubgroup( U ); > for u in U.generators do > for v in V.generators do > c := Comm( u, v ); > if not c in C then > C := Closure( C, c ); > fi; > od; > od; > return NormalClosure( Closure( U, V ), C ); > end;;
So it should be possible to use this function for the new type of groups. The mechanism to do this is called inheritance. How it works is described in About Defining New Domains, but basically the programmer just copies the generic functions from the generic group operations record into the operations record for his new type of groups.
The generic functions are also called default functions, because they are used by default, unless the programmer overlaid them for the new type of groups.
There is another mechanism through which work can be simplified. It is
called delegation. Suppose that a generic function works for the new
type of groups, but that some special cases can be handled more
efficiently for the new type of groups. Then it is possible to handle
only those cases and delegate the general cases back to the generic
function. An example of this is the function that computes the orbit of
a point under a permutation group. If the point is an integer then the
generic algorithm can be improved by keeping a second list that remembers
which points have already been seen. The other cases (remember that
Orbit
can also be used for other operations, e.g., the operation of a
permutation group on pairs of points or the operations on subgroups by
conjugation) are delegated back to the generic function. How this is
done can be seen in the following definition.
gap> PermGroupOps.Orbit := function ( G, d, opr ) > local orb, # orbit of <d> under <G>, result > max, # largest point moved by the group <G> > new, # boolean list indicating if a point is new > gen, # one generator of the group <G> > pnt, # one point in the orbit <orb> > img; # image of <pnt> under <gen> > > # standard operation > if opr = OnPoints and IsInt(d) then > > # get the largest point <max> moved by the group <G> > max := 0; > for gen in G.generators do > if max < LargestMovedPointPerm(gen) then > max := LargestMovedPointPerm(gen); > fi; > od; > > # handle fixpoints > if not d in [1..max] then > return [ d ]; > fi; > > # start with the singleton orbit > orb := [ d ]; > new := BlistList( [1..max], [1..max] ); > new[d] := false; > > # loop over all points found > for pnt in orb do > for gen in G.generators do > img := pnt ^ gen; > if new[img] then > Add( orb, img ); > new[img] := false; > fi; > od; > od; > > # other operation, delegate back on default function > else > orb := GroupOps.Orbit( G, d, opr ); > fi; > > # return the orbit <orb> > return orb; > end;;
Inheritance and delegation allow the programmer to implement a new type of groups by merely specifying how those groups differ from generic groups. This is far less work than having to implement all possible functions (apart from the problem that in this case it is very likely that the programmer would forget some of the more exotic functions).
To make all this clearer let us look at an extended example to show you
how a computation in a domain may use default and special functions to
achieve its goal. Suppose you defined g
, x
, and y
as follows.
gap> g := SymmetricGroup( 8 );; gap> x := [ (2,7,4)(3,5), (1,2,6)(4,8) ];; gap> y := [ (2,5,7)(4,6), (1,5)(3,8,7) ];;
Now you ask for an element of g
that conjugates x
to y
, i.e., a
permutation on 8 points that takes (2,7,4)(3,5)
to (2,5,7)(4,6)
and
(1,2,6)(4,8)
to (1,5)(3,8,7)
. This is done as follows (see
RepresentativeOperation and Other Operations).
gap> RepresentativeOperation( g, x, y, OnTuples ); (1,8)(2,7)(3,4,5,6)
Now lets look at what happens step for step. First
RepresentativeOperation
is called. After checking the arguments it
calls the function g.operations.RepresentativeOperation
, which is the
function SymmetricGroupOps.RepresentativeOperation
, passing the
arguments g
, x
, y
, and OnTuples
.
SymmetricGroupOps.RepresentativeOperation
handles a lot of cases
special, but the operation on tuples of permutations is not among them.
Therefore it delegates this problem to the function that it overlays,
which is PermGroupOps.RepresentativeOperation
.
PermGroupOps.RepresentativeOperation
also does not handle this special
case, and delegates the problem to the function that it overlays, which
is the default function called GroupOps.RepresentativeOperation
.
GroupOps.RepresentativeOperation
views this problem as a general tuples
problem, i.e., it does not care whether the points in the tuples are
integers or permutations, and decides to solve it one step at a time. So
first it looks for an element taking (2,7,4)(3,5)
to (2,5,7)(4,6)
by
calling RepresentativeOperation( g, (2,7,4)(3,5), (2,5,7)(4,6) )
.
RepresentativeOperation
calls g.operations.RepresentativeOperation
next, which is the function SymmetricGroupOps.RepresentativeOperation
,
passing the arguments g
, (2,7,4)(3,5)
, and (2,5,7)(4,6)
.
SymmetricGroupOps.RepresentativeOperation
can handle this case. It
knows that g
contains every permutation on 8 points, so it contains
(3,4,7,5,6)
, which obviously does what we want, namely it takes x[1]
to y[1]
. We will call this element t
.
Now GroupOps.RepresentativeOperation
(see above) looks for an s
in
the stabilizer of x[1]
taking x[2]
to y[2]^(t^-1)
, since then for
r=s*t
we have x[1]^r = (x[1]^s)^t = x[1]^t = y[1]
and also
x[2]^r = (x[2]^s)^t = (y[2]^(t^-1))^t = y[2]
. So the next step
is to compute the stabilizer of x[1]
in g
. To do this it calls
Stabilizer( g, (2,7,4)(3,5) )
.
Stabilizer
calls g.operations.Stabilizer
, which is
SymmetricGroupOps.Stabilizer
, passing the arguments g
and
(2,7,4)(3,5)
. SymmetricGroupOps.Stabilizer
detects that the second
argument is a permutation, i.e., an element of the group, and calls
Centralizer( g, (2,7,4)(3,5) )
. Centralizer
calls the function
g.operations.Centralizer
, which is SymmetricGroupOps.Centralizer
,
again passing the arguments g
, (2,7,4)(3,5)
.
SymmetricGroupOps.Centralizer
again knows how centralizer in
symmetric groups look, and after looking at the permutation
(2,7,4)(3,5)
sharply for a short while returns the centralizer as
Subgroup( g, [ (1,6), (6,8), (2,7,4), (3,5) ] )
, which we will call
c
. Note that c
is of course not a symmetric group, therefore
SymmetricGroupOps.Subgroup
gives it PermGroupOps
as operations record
and not SymmetricGroupOps
.
As explained above GroupOps.RepresentativeOperation
needs an element of
c
taking x[2]
((1,2,6)(4,8)
) to y[2]^(t^-1)
((1,7)(4,6,8)
).
So RepresentativeOperation( c, (1,2,6)(4,8), (1,7)(4,6,8) )
is called.
RepresentativeOperation
in turn calls the function
c.operations.RepresentativeOperation
, which is, since c
is a
permutation group, the function PermGroupOps.RepresentativeOperation
,
passing the arguments c
, (1,2,6)(4,8)
, and (1,7)(4,6,8)
.
PermGroupOps.RepresentativeOperation
detects that the points are
permutations and and performs a backtrack search through c
. It finds
and returns (1,8)(2,4,7)(3,5)
, which we call s
.
Then GroupOps.RepresentativeOperation
returns r = s*t =
(1,8)(2,7)(3,6)(4,5)
, and we are done.
In this example you have seen how functions use the structure of their
domain to solve a problem most efficiently, for example
SymmetricGroupOps.RepresentativeOperation
but also the backtrack search
in PermGroupOps.RepresentativeOperation
, how they use other functions,
for example SymmetricGroupOps.Stabilizer
called Centralizer
, and how
they delegate cases which they can not handle more efficiently back to
the function they overlaid, for example
SymmetricGroupOps.RepresentativeOperation
delegated to
PermGroupOps.RepresentativeOperation
, which in turn delegated to to the
function GroupOps.RepresentativeOperation
.
If you think this whole mechanism using dispatcher functions and the operations record is overly complex let us look at some of the alternatives. This is even more technical than the previous part of this section so you may want to skip the remainder of this section.
One alternative would be to let the dispatcher know about the various
types of domains, test which category a domain lies in, and dispatch to
an appropriate function. Then we would not need an operations record.
The dispatcher function CommutatorSubgroup
would then look as follows.
Note this is not how CommutatorSubgroup
is implemented in GAP.
CommutatorSubgroup := function ( G ) local C; if IsAgGroup( G ) then C := CommutatorSubgroupAgGroup( G ); elif IsMatGroup( G ) then C := CommutatorSubgroupMatGroup( G ); elif IsPermGroup( G ) then C := CommutatorSubgroupPermGroup( G ); elif IsFpGroup( G ) then C := CommutatorSubgroupFpGroup( G ); elif IsFactorGroup( G ) then C := CommutatorSubgroupFactorGroup( G ); elif IsDirectProduct( G ) then C := CommutatorSubgroupDirectProduct( G ); elif IsDirectProductAgGroup( G ) then C := CommutatorSubgroupDirectProductAgGroup( G ); elif IsSubdirectProduct( G ) then C := CommutatorSubgroupSubdirectProduct( G ); elif IsSemidirectProduct( G ) then C := CommutatorSubgroupSemidirectProduct( G ); elif IsWreathProduct( G ) then C := CommutatorSubgroupWreathProduct( G ); elif IsGroup( G ) then C := CommutatorSubgroupGroup( G ); else Error("<G> must be a group"); fi; return C; end;
You already see one problem with this approach. The number of cases that the dispatcher functions would have to test is simply to large. It is even worse for set theoretic functions, because they would have to handle all different types of domains (currently about 30).
The other problem arises when a programmer implements a new domain. Then he would have to rewrite all dispatchers and add a new case to each. Also the probability that the programmer forgets one dispatcher is very high.
Another problem is that inheritance becomes more difficult. Instead of just copying one operations record the programmer would have to copy each function that should be inherited. Again the probability that he forgets one is very high.
Another alternative would be to do completely without dispatchers. In
this case there would be the functions CommutatorSugroupAgGroup
,
CommutatorSubgroupPermGroup
, etc., and it would be your responsibility
to call the right function. For example to compute the size of a
permutation group you would call SizePermGroup
and to compute the size
of a coset you would call SizeCoset
(or maybe even
SizeCosetPermGroup
).
The most obvious problem with this approach is that it is much more cumbersome. You would always have to know what kind of domain you are working with and which function you would have to call.
Another problem is that writing generic functions would be impossible.
For example the above generic implementation of CommutatorSubgroup
could not work, because for a concrete group it would have to call
ClosurePermGroup
or ClosureAgGroup
etc.
If generic functions are impossible, inheritance and delegation can not be used. Thus for each type of domain all functions must be implemented. This is clearly a lot of work, more work than we are willing to do.
So we argue that our mechanism is the easiest possible that serves the following two goals. It is reasonably convenient for you to use. It allows us to implement a large (and ever increasing) number of different types of domains.
This may all sound a lot like object oriented programming to you. This is not surprising because we want to solve the same problems that object oriented programming tries to solve. Let us briefly discuss the similarities and differences to object oriented programming, taking C++ as an example (because it is probably the widest known object oriented programming language nowadays). This discussion is very technical and again you may want to skip the remainder of this section.
Let us first recall the problems that the GAP mechanism wants to handle.
For object oriented programming the problems are the same, though the names used are different. We talk about domains, object oriented programming talks about objects, and we talk about categories, object oriented programming talks about classes.
In GAP the first problem is solved by representing all domains using records. Actually because GAP does not perform strong static type checking each variable can hold objects of arbitrary type, so it would even be possible to represent some domains using lists or something else. But then, where would we put the operations record?
C++ does something similar. Objects are represented by struct
-s or
pointers to structures. C++ then allows that a pointer to an object of a
base class actually holds a pointer to an object of a derived class.
In GAP the second problem is solved by the dispatchers and the operations record. The operations record of a given domain holds the methods that should be applied to that domain, and the dispatcher does nothing but call this method.
In C++ it is again very similar. The difference is that the dispatcher
only exists conceptually. If the compiler can already decide which
method will be executed by a given call to the dispatcher it directly
calls this function. Otherwise (for virtual functions that may be
overlaid in derived classes) it basically inlines the dispatcher. This
inlined code then dispatches through the so--called virtual method
table (vmt
). Note that this virtual method table is the same as the
operations record, except that it is a table and not a record.
In GAP the third problem is solved by inheritance and delegation. To
inherit functions you simply copy them from the operations record of
domains of the old category to the operations record of domains of the
new category. Delegation to a method of a larger category is done by
calling super-category-operations-record.function
C++ also supports inheritance and delegation. If you derive a class from
a base class, you copy the methods from the base class to the derived
class. Again this copying is only done conceptually in C++. Delegation
is done by calling a qualified function base-class::function
.
Now that we have seen the similarities, let us discuss the differences.
The first differences is that GAP is not an object oriented programming language. We only programmed the library in an object oriented way using very few features of the language (basically all we need is that GAP has no strong static type checking, that records can hold functions, and that records can grow dynamically). Following Stroustrup's convention we say that the GAP language only enables object oriented programming, but does not support it.
The second difference is that C++ adds a mechanism to support data
hiding. That means that fields of a struct
can be private. Those
fields can only be accessed by the functions belonging to this class (and
friend
functions). This is not possible in GAP. Every field of
every domain is accessible. This means that you can also modify those
fields, with probably catastrophic results.
The final difference has to do with the relation between categories and their domains and classes and their objects. In GAP a category is a set of domains, thus we say that a domain is an element of a category. In C++ (and most other object oriented programming languages) a class is a prototype for its objects, thus we say that an object is an instance of the class. We believe that GAP's relation better resembles the mathematical model.
In this section you have seen that domains are represented by domain records, and that you can therefore access all information that GAP has about a certain domain. The following sections in this chapter discuss how new domains can be created (see About Defining New Domains, and About Defining New Parametrized Domains) and how you can even define a new type of elements (see About Defining New Group Elements).
GAP 3.4.4