This chapter gives a few examples of how one can extend the functionality of GAP.
They are arranged in ascending difficulty. We show how to install new methods, add new operations and attributes and how to implement new features using categories and representations. (As we do not introduce completely new kinds of objects in these example it will not be necessary to declare any families.) Finally we show a simple way how to create new objects with an own arithmetic.
The examples given are all very rudimentary – no particular error checks are performed and the user interface sometimes is quite clumsy. These examples may be constructed for presentation purposes only and they do not necessarily constitute parts of the GAP library.
Even more complex examples that create whole classes of objects anew will be given in the following two chapters 81 and 82.
The easiest case is the addition of a new algorithm as a method for an existing operation for the existing structures.
For example, assume we wanted to implement a better method for computing the exponent of a nilpotent group (it is the product of the exponents of the Sylow subgroups).
The first task is to find which operation is used by GAP (it is Exponent
(39.16-2)) and how it is declared. We can find this in the Reference Manual (in our particular case in section 39.16) and the declaration in the library file lib/grp.gd
. The easiest way to find the place of the declaration is usually to grep
over all .gd
and .g
files, see section 83.
In our example the declaration in the library is:
DeclareAttribute("Exponent",IsGroup);
Similarly we find that the filter IsNilpotentGroup
(39.15-3) represents the concept of being nilpotent.
We then write a function that implements the new algorithm which takes the right set of arguments and install it as a method. In our example this installation would be:
InstallMethod(Exponent,"for nilpotent groups", [IsGroup and IsNilpotent], function(G) [function body omitted] end);
We have left out the optional rank argument of InstallMethod
(78.3-1), which normally is a wise choice –GAP automatically uses an internal ranking based on the filters that is only offset by the given rank. So our method will certainly be regarded as better
than a method that has been installed for mere groups or for solvable groups but will be ranked lower than the library method for abelian groups.
That's all. Using 7.2-1 we can check for a nilpotent group that indeed our new method will be used.
When testing, remember that the method selection will not check for properties that are not known. (This is done internally by checking the property tester first.) Therefore the method would not be applicable for the group g
in the following definition but only for the –mathematically identical but endowed with more knowledge by GAP– group h
. (Section 80.3 shows a way around this.)
gap> g:=Group((1,2),(1,3)(2,4));; gap> h:=Group((1,2),(1,3)(2,4));; gap> IsNilpotentGroup(h); # enforce test true gap> HasIsNilpotentGroup(g); false gap> HasIsNilpotentGroup(h); true
Let's now look at a slightly more complicated example: We want to implement a better method for computing normalizers in a nilpotent permutation group. (Such an algorithm can be found for example in [LRW97].)
We already know IsNilpotentGroup
(39.15-3), the filter IsPermGroup
(43.1-1) represents the concept of being a group of permutations.
GAP uses Normalizer
(39.11-1) to compute normalizers, however the declaration is a bit more complicated. In the library we find
InParentFOA( "Normalizer", IsGroup, IsObject, NewAttribute );
The full mechanism of InParentFOA
(85.2-1) is described in chapter 85, however for our purposes it is sufficient to know that for such a function the actual work is done by an operation NormalizerOp
, an underlying operation for Normalizer
(39.11-1) (and all the complications are just there to be able to remember certain results) and that the declaration of this operation is given by the first arguments, it would be:
DeclareOperation( "NormalizerOp", [IsGroup, IsObject] );
This time we decide to enter a non-default family predicate in the call to InstallMethod
(78.3-1). We could just leave it out as in the previous call; this would yield the default value, the function ReturnTrue
(5.4-1) of arbitrary many arguments which always returns true
. However, then the method might be called in some cases of inconsistent input (for example matrix groups in different characteristics) that ought to fall through the method selection to raise an error.
In our situation, we want the second group to be a subgroup of the first, so necessarily both must have the same family and we can use IsIdenticalObj
(12.5-1) as family predicate.
Now we can install the method. Again this manual is lazy and does not show you the actual code:
InstallMethod(NormalizerOp,"for nilpotent permutation groups",IsIdenticalObj, [IsPermGroup and IsNilpotentGroup, IsPermGroup and IsNilpotentGroup], function(G,U) [ function body omitted ] end);
It might be that the operation has been defined so far only for a set of objects that is too restrictive for our purposes (or we want to install a method that takes another number of arguments). If this is the case, the call to InstallMethod
(78.3-1) causes an error message. We can avoid this by using InstallOtherMethod
(78.3-2) instead. It is also possible to re-declare an operation with another number of arguments and/or different filters for its arguments.
As mentioned in Section 78.4, GAP does not check unknown properties to test whether a method might be applicable. In some cases one wants to enforce this, however, because the gain from knowing the property outweighs the cost of its determination.
In this situation one has to install a method without the additional property (so it can be tried even if the property is not yet known) and at high rank (so it will be used before other methods). The first thing to do in the actual function then is to test the property and to bail out with TryNextMethod
(78.5-1) if it turns out to be false
.
The above Exponent
(39.16-2) example thus would become:
InstallMethod(Exponent,"test abelianity", [IsGroup], 50,# enforced high rank function(G) if not IsAbelian(G) then TryNextMethod(); fi; [remaining function body omitted] end);
The value 50
used in this example is quite arbitrary. A better way is to use values that are given by the system inherently: We want this method still to be ranked as high, as if it had the IsAbelian
(35.4-9) requirement. So we have GAP compute the extra rank of this:
InstallMethod(Exponent,"test abelianity", [IsGroup], # enforced absolute rank of `IsGroup and IsAbelian' installation: Subtract # the rank of `IsGroup' and add the rank of `IsGroup and IsAbelian': RankFilter(IsGroup and IsAbelian) -RankFilter(IsGroup), function(G)
the slightly complicated construction of addition and subtraction is necessary because IsGroup
(39.2-7) and IsAbelian
(35.4-9) might imply the same elementary filters which we otherwise would count twice.
A somehow similar situation occurs with matrix groups. Most methods for matrix groups are only applicable if the group is known to be finite.
However we should not enforce a finiteness test early (someone else later might install good methods for infinite groups while the finiteness test would be too expensive) but just before GAP would give a no method found
error. This is done by redispatching, see 78.6. For example to enforce such a final finiteness test for normalizer calculations could be done by:
RedispatchOnCondition(NormalizerOp,IsIdenticalObj, [IsMatrixGroup,IsMatrixGroup],[IsFinite,IsFinite],0);
Next, we will consider how to add own operations. As an example we take the Sylow normalizer in a group of a given prime. This operation gets two arguments, the first has to be a group, the second a prime number.
There is a function IsPrimeInt
(14.4-2), but no property for being prime (which would be pointless as integers cannot store property values anyhow). So the second argument gets specified only as positive integer:
SylowNormalizer:=NewOperation("SylowNormalizer",[IsGroup,IsPosInt]);
(Note that we are using NewOperation
(78.1-4) instead of DeclareOperation
(78.1-5) as used in the library. The only difference other than that DeclareOperation
(78.1-5) saves some typing, is that it also protects the variables against overwriting. When testing code (when one probably wants to change things) this might be restricting. If this does not bother you, you can use
DeclareOperation("SylowNormalizer",[IsGroup,IsPosInt]);
as well.)
The filters IsGroup
(39.2-7) and IsPosInt
(14.2-2) given are only used to test that InstallMethod
(78.3-1) installs methods with suitable arguments and will be completely ignored when using InstallOtherMethod
(78.3-2). Technically one could therefore simply use IsObject
(12.1-1) for all arguments in the declaration. The main point of using more specific filters here is to help documenting with which arguments the function is to be used (so for example a call SylowNormalizer(5,G)
would be invalid).
Of course initially there are no useful methods for newly declared operations; you will have to write and install them yourself.
If the operation only takes one argument and has reproducible results without side effects, it might be worth declaring it as an attribute instead; see Section 80.5.
Now we look at an example of how to add a new attribute. As example we consider the set of all primes that divide the size of a group.
First we have to declare the attribute:
PrimesDividingSize:=NewAttribute("PrimesDividingSize",IsGroup);
(See NewAttribute
(13.5-3)). This implicitly declares attribute tester and setter, it is convenient however to assign these to variables as well:
HasPrimesDividingSize:=Tester(PrimesDividingSize); SetPrimesDividingSize:=Setter(PrimesDividingSize);
Alternatively, there is a declaration command DeclareAttribute
(13.5-4) that executes all three assignments simultaneously and protects the variables against overwriting:
DeclareAttribute("PrimesDividingSize",IsGroup);
Next we have to install method(s) for the attribute that compute its value. (This is not strictly necessary. We could use the attribute also without methods only for storing and retrieving information, but calling it for objects for which the value is not known would produce a no method found
error.) For this purpose we can imagine the attribute simply as an one-argument operation:
InstallMethod(PrimesDividingSize,"for finite groups", [IsGroup and IsFinite], function(G) return PrimeDivisors(Size(G)); end);
The function installed must always return a value (or call TryNextMethod
(78.5-1)). If the object is in the representation IsAttributeStoringRep
(13.5-5) this return value once computed will be automatically stored and retrieved if the attribute is called a second time. We don't have to call setter or tester ourselves. (This storage happens by GAP internally calling the attribute setter with the return value of the function. Retrieval is by a high-ranking method which is installed under the condition HasPrimesDividingSize
. This method was installed automatically when the attribute was declared.)
Next, we look at the implementation of a new representation of existing objects. In most cases we want to implement this representation only for efficiency reasons while keeping all the existing functionality.
For example, assume we wanted (following [Wie69]) to implement permutation groups defined by relations.
Next, we have to decide a few basics about the representation. All existing permutation groups in the library are attribute storing and we probably want to keep this for our new objects. Thus the representation must be a subrepresentation of IsComponentObjectRep
(13.4-1) and IsAttributeStoringRep
(13.5-5). Furthermore we want each object to be a permutation group and we can imply this directly in the representation.
We also decide that we store the degree (the largest point that might be moved) in a component degree
and the defining relations in a component relations
(we do not specify the format of relations here. In an actual implementation one would have to design this as well, but it does not affect the declarations this chapter is about).
DeclareRepresentation( "IsPermutationGroupByRelations", IsComponentObjectRep and IsAttributeStoringRep and IsPermGroup, ["degree","relations"]);
(If we wanted to implement sparse matrices we might for example rather settle for a positional object in which we store a list of the nonzero entries.)
We can make the new representation a subrepresentation of an existing one. In such a case of course we have to provide all structure of this parent
representation as well.
Next we need to check in which family our new objects will be. This will be the same family as of every other permutation group, namely the CollectionsFamily(PermutationsFamily)
(where the family PermutationsFamily = FamilyObj((1,2,3))
has been defined already in the library).
Now we can write a function to create our new objects. Usually it is helpful to look at functions from the library that are used in similar situations (for example GroupByGenerators
(39.2-2) in our case) to make sure we have not forgotten any further requirements in the declaration we might have to add here. However in most cases the function is straightforward:
PermutationGroupByRelations:=function(degree,relations) local g g:=Objectify(NewType(CollectionsFamily(PermutationsFamily), IsPermutationGroupByRelations), rec(degree:=degree,relations:=relations)); return g; end;
It also is a good idea to install a PrintObj
(6.3-5) and possibly also a ViewObj
(6.3-5) method –otherwise testing becomes quite hard:
InstallMethod(PrintObj,"for perm grps. given by relations", [IsPermutationGroupByRelations], function(G) Print("PermutationGroupByRelations(", G!.degree,",",G!.relations,")"); end);
Next we have to write enough methods for the new representation so that the existing algorithms can be used. In particular we will have to implement methods for all operations for which library or kernel provides methods for the existing (alternative) representations. In our particular case there are no such methods. (If we would have implemented sparse matrices we would have had to implement methods for the list access and assignment functions, see 21.2.) However the existing way permutation groups are represented is by generators. To be able to use the existing machinery we want to be able to obtain a generating set also for groups in our new representation. This can be done (albeit not very effectively) by a stabilizer calculation in the symmetric group given by the degree
component. The operation function to use is probably a bit complicated and will depend on the format of the relations
(we have not specified in this example). In the following method we use operationfunction
as a placeholder;
InstallMethod(GeneratorsOfGroup,"for perm grps. given by relations", [IsPermutationGroupByRelations], function(G) local S,U; S:=SymmetricGroup(G!.degree); U:=Stabilizer(S,G!.relations, operationfunction ); return GeneratorsOfGroup(U); end);
This is all we must do. Of course for performance reasons one might want to install methods for further operations as well.
In the last section we introduced two new components, G!.degree
and G!.relations
. Technically, we could have used attributes instead. There is no clear distinction which variant is to be preferred: An attribute expresses part of the functionality available to certain objects (and thus could be computed later and probably even for a wider class of objects), a component is just part of the internal definition of an object.
So if the data is of general interest
, if we want the user to have access to it, attributes are preferable. Moreover, attributes can be used by the method selection (by specifying the filter HasAttr
for an attribute Attr
). They provide a clean interface and their immutability makes it safe to hand the data to a user who potentially could corrupt a components entries.
On the other hand more technical
data (say the encoding of a sparse matrix) is better hidden from the user in a component, as declaring it as an attribute would not give any advantage.
Resource-wise, attributes need more memory (the attribute setter and tester are implicitly declared, and one filter bit is required), the attribute access is one further function call in the kernel, thus components might be an immeasurable bit faster.
Now we look how to implement a new concept for existing objects and fit this in the method selection. Three examples that will be made more explicit below would be groups for which a length
of elements (as a word in certain generators) is defined, groups that can be decomposed as a semidirect product and M-groups.
In each case we have two possibilities for the declaration. We can either declare it as a property or as a category. Both are eventually filter(s) and in this way indistinguishable for the method selection. However, the value of a property for a particular object can be unknown at first and later in the session be computed (to be true
or false
). This is implemented by reserving two filters for each property, one indicating whether the property value is known, and one, provided the value is known, to indicate the actual boolean value. Contrary to this, the decision whether or not an object lies in a category is taken at creation time and this is implemented using a single filter.
Properties also are attributes: If a property value is not known for an object, GAP tries to find a method to compute the property value. If no suitable method is found, an error is raised.
An object is in a category if it has been created in it. Testing the category for an object simply returns this value. Existing objects cannot enter a new category later in life. This means that in most cases one has to write own code to create objects in a new category.
If we want to implement a completely new concept so that new operations are defined only for the new objects –for example bialgebras for which a second scalar multiplication is defined– usually a category is chosen.
Technically, the behaviour of the category IsXYZ
, declared as subcategory of IsABC
is therefore exactly the same as if we would declare IsXYZ
to be a property for IsABC
and install the following method:
InstallMethod(IsXYZ,"return false if not known",[IsABC],ReturnFalse);
(The word category
also has a well-defined mathematical meaning, but this does not need to concern us at this point. The set of objects which is defined to be a (GAP) category does not need to be a category in the mathematical sense, vice versa not every mathematical category is declared as a (GAP) category.)
Eventually the choice between category and property often becomes a matter of taste or style.
Sometimes there is even a third possibility (if you have GAP 3 experience this might reflect most closely an object whose operations record is
): We might want to indicate this new concept simply by the fact that certain attributes are set. In this case we could simply use the respective attribute tester(s).XYOps
The examples given below each give a short argument why the respective solution was chosen, but one could argue as well for other choices.
M-groups are finite groups for which all irreducible complex representations are induced from linear representations of subgroups, it turns out that they are all solvable and that every supersolvable group is an M-group. See [Isa76] for further details.
Solvability and supersolvability both are testable properties. We therefore declare IsMGroup
as a property for solvable groups:
IsMGroup:=NewProperty("IsMGroup",IsSolvableGroup);
The filter IsSolvableGroup
(39.15-6) in this declaration only means that methods for IsMGroup
by default can only be installed for groups that are (and know to be) solvable (though they could be installed for more general situations using InstallOtherMethod
(78.3-2)). It does not yet imply that M-groups are solvable. We must do this deliberately via an implication and we use the same technique to imply that every supersolvable group is an M-group.
InstallTrueMethod(IsSolvableGroup,IsMGroup); InstallTrueMethod(IsMGroup,IsSupersolvableGroup);
Now we might install a method that tests for solvable groups whether they are M-groups:
InstallMethod(IsMGroup,"for solvable groups",[IsSolvableGroup], function(G) [... code omitted. The function must return `true' or `false' ...] end);
Note that this example of declaring the IsMGroup
property for solvable groups is not a part of the GAP library, which uses a similar but different filter IsMonomialGroup
(39.15-9).
Our second example is that of groups for whose elements a word length is defined. (We assume that the word length is only defined in the context of the group with respect to a preselected generating set but not for single elements alone. However we will not delve into any details of how this length is defined and how it could be computed.)
Having a word length is a feature which enables other operations (for example a word length
function). This is exactly what categories are intended for and therefore we use one.
First, we declare the category. All objects in this category are groups and so we inherit the supercategory IsGroup
(39.2-7):
DeclareCategory("IsGroupWithWordLength",IsGroup);
We also define the operation which is enabled
by this category, the word length of a group element, which is defined for a group and an element (remember that group elements are described by the category IsMultiplicativeElementWithInverse
(31.14-13)):
DeclareOperation("WordLengthOfElement",[IsGroupWithWordLength, IsMultiplicativeElementWithInverse]);
We then would proceed by installing methods to compute the word length in concrete cases and might for example add further operations to get shortest words in cosets.
The third example is groups which have a (nontrivial) decomposition as a semidirect product. If this information has been found out, we want to be able to use it in algorithms. (Thus we do not only need the fact that there is a decomposition, but also the decomposition itself.)
We also want this to be applicable to every group and not only for groups which have been explicitly constructed via SemidirectProduct
(49.2-1).
Instead we simply declare an attribute SemidirectProductDecomposition
for groups. (Again, in this manual we don't go in the details of how such an decomposition would look like).
DeclareAttribute("SemidirectProductDecomposition",IsGroup);
If a decomposition has been found, it can be stored in a group using SetSemidirectProductDecomposition
. (At the moment all groups in GAP are attribute storing.)
Methods that rely on the existence of such a decomposition then get installed for the tester filter HasSemidirectProductDecomposition
.
Finally let's look at a way to create new objects with a user-defined arithmetic such that one can form for example groups, rings or vector spaces of these elements. This topic is discussed in much more detail in chapter 82, in this section we present a simple approach that may be useful to get started but does not permit you to exploit all potential features.
The basic design is that the user designs some way to represent her objects in terms of GAPs built-in types, for example as a list or a record. We call this the defining data
of the new objects. Also provided are functions that perform arithmetic on this defining data
, that is they take objects of this form and return objects that represent the result of the operation. The function ArithmeticElementCreator
(80.9-1) then is called to provide a wrapping such that proper new GAP-objects are created which can be multiplied etc. with the default infix operations such as *
.
‣ ArithmeticElementCreator ( spec ) | ( function ) |
offers a simple interface to create new arithmetic elements by providing functions that perform addition, multiplication and so forth, conforming to the specification spec. ArithmeticElementCreator
creates a new category, representation and family for the new arithmetic elements being defined, and returns a function which takes the defining data
of an element and returns the corresponding new arithmetic element.
spec is a record with one or more of the following components:
ElementName
string used to identify the new type of object. A global identifier IsElementName
will be defined to indicate a category for these now objects. (Therefore it is not clever to have blanks in the name). Also a collections category is defined. (You will get an error message if the identifier IsElementName
is already defined.)
Equality
, LessThan
, One
, Zero
,
Multiplication
, Inverse
, Addition
,
AdditiveInverse
functions defining the arithmetic operations. The functions interface on the level of defining data
, the actual methods installed will perform the unwrapping and wrapping as objects. Components are optional, but of course if no multiplication is defined elements cannot be multiplied and so forth.
There are default methods for Equality
and LessThan
which simply calculate on the defining data. If one is defined, it must be ensured that the other is compatible (so that a < b implies not (a = b))
Print
a function which prints the object. By default, just the defining data is printed.
MathInfo
filters determining the mathematical properties of the elements created. A typical value is for example IsMultiplicativeElementWithInverse
for group elements.
RepInfo
filters determining the representational properties of the elements created. The objects created are always component objects, so in most cases the only reasonable option is IsAttributeStoringRep
to permit the storing of attributes.
All components are optional and will be filled in with default values (though of course an empty record will not result in useful objects).
Note that the resulting objects are not equal to their defining data (even though by default they print as only the defining data). The operation UnderlyingElement
can be used to obtain the defining data of such an element.
As the first example we look at subsets of { 1, ..., 4 } and define an addition
as union and multiplication
as intersection. These operations are both commutative and we want the resulting elements to know this.
We therefore use the following specification:
gap> # the whole set gap> w := [1,2,3,4]; [ 1, 2, 3, 4 ] gap> PosetElementSpec :=rec( > # name of the new elements > ElementName := "PosetOn4", > # arithmetic operations > One := a -> w, > Zero := a -> [], > Multiplication := function(a, b) return Intersection(a, b); end, > Addition := function(a, b) return Union(a, b); end, > # Mathematical properties of the elements > MathInfo := IsCommutativeElement and > IsAssociativeElement and > IsAdditivelyCommutativeElement > );; gap> mkposet := ArithmeticElementCreator(PosetElementSpec); function( x ) ... end
Now we can create new elements, perform arithmetic on them and form domains:
gap> a := mkposet([1,2,3]); [ 1, 2, 3 ] gap> CategoriesOfObject(a); [ "IsExtAElement", "IsNearAdditiveElement", "IsNearAdditiveElementWithZero", "IsAdditiveElement", "IsExtLElement", "IsExtRElement", "IsMultiplicativeElement", "IsMultiplicativeElementWithOne", "IsAssociativeElement", "IsAdditivelyCommutativeElement", "IsCommutativeElement", "IsPosetOn4" ] gap> a=[1,2,3]; false gap> UnderlyingElement(a)=[1,2,3]; true gap> b:=mkposet([2,3,4]); [ 2, 3, 4 ] gap> a+b; [ 1 .. 4 ] gap> a*b; [ 2, 3 ] gap> s:=Semigroup(a,b); <commutative semigroup with 2 generators> gap> Size(s); 3
The categories IsPosetOn4
and IsPosetOn4Collection
can be used to install methods specific to the new objects.
gap> IsPosetOn4Collection(s); true
generated by GAPDoc2HTML