This chapter introduces you to the **GAP** system. It describes how to
start **GAP** (you may have to ask your system administrator to install it
correctly) and how to leave it. Then a step by step introduction should
give you an impression of how the **GAP** system works. Further sections
will give an overview about the features of **GAP**. After reading this
chapter the reader should know what kind of problems can be handled with
**GAP** and how they can be handled.

There is some repetition in this chapter and much of the material is repeated in later chapters in a more compact and precise way. Yes, there are even some little inaccuracies in this chapter simplifying things for better understanding. It should be used as a tutorial introduction while later chapters form the reference manual.

**GAP** is an interactive system. It continuously executes a
read--evaluate--print cycle. Each expression you type at the keyboard is
read by **GAP**, evaluated, and then the result is printed.

The interactive nature of **GAP** allows you to type an expression at the
keyboard and see its value immediately. You can define a function and
apply it to arguments to see how it works. You may even write whole
programs containing lots of functions and test them without leaving the
program.

When your program is large it will be more convenient to write it on a
file and then read that file into **GAP**. Preparing your functions in a
file has several advantages. You can compose your functions more
carefully in a file (with your favorite text editor), you can correct
errors without retyping the whole function and you can keep a copy for
later use. Moreover you can write lots of comments into the program
text, which are ignored by **GAP**, but are very useful for human readers
of your program text.

**GAP** treats input from a file in the same way that it treats input from
the keyboard.

The printed examples in this first chapter encourage you to try running
**GAP** on your computer. This will support your feeling for **GAP** as a
tool, which is the leading aim of this chapter. Do not believe any
statement in this chapter so long as you cannot verify it for your own
version of **GAP**. You will learn to distinguish between small
deviations of the behavior of your personal **GAP** from the printed
examples and serious nonsense.

Since the printing routines of **GAP** are in some sense machine dependent
you will for instance encounter a different layout of the printed objects
in different environments. But the contents should always be the same.

In case you encounter serious nonsense it is highly recommended that you
send a bug report to `gap-forum@samson.math.rwth-aachen.de`

.

If you read this introduction on-line you should now enter `?`

to read
the next section.

- About Conventions
- About Starting and Leaving GAP
- About First Steps
- About Help
- About Syntax Errors
- About Constants and Operators
- About Variables and Assignments
- About Functions
- About Lists
- About Identical Lists
- About Sets
- About Vectors and Matrices
- About Records
- About Ranges
- About Loops
- About Further List Operations
- About Writing Functions
- About Groups
- About Operations of Groups
- About Finitely Presented Groups and Presentations
- About Fields
- About Matrix Groups
- About Domains and Categories
- About Mappings and Homomorphisms
- About Character Tables
- About Group Libraries
- About the Implementation of Domains
- About Defining New Domains
- About Defining New Parametrized Domains
- About Defining New Group Elements

Throughout this manual both the input given to **GAP** and the output that
**GAP** returns are printed in `typewriter`

font just as if they were
typed at the keyboard.

An `italic` font is used for keys that have no printed representation,
such as e.g. the `newline` key and the `ctl` key. This font is also
used for the formal parameters of functions that are described in later
chapters.

A combination like `ctl`-`P`

means pressing both keys, that is holding
the control key `ctl` and pressing the key `P`

while `ctl` is still
pressed.

New terms are introduced in **bold face**.

In most places **whitespace** characters (i.e. `space`s, `tab`s and
`newline`s) are insignificant for the meaning of **GAP** input.
Identifiers and keywords must however not contain any whitespace. On the
other hand, sometimes there must be whitespace around identifiers and
keywords to separate them from each other and from numbers. We will use
whitespace to format more complicated commands for better readability.

A **comment** in **GAP** starts with the symbol `#`

and continues to the
end of the line. Comments are treated like whitespace by **GAP**.

Besides of such comments which are part of the input of a **GAP** session,
we use additional comments which are part of the manual description, but
not of the respective **GAP** session. In the printed version of this
manual these comments will be printed in a normal font for better
readability, hence they start with the symbol #.

The examples of **GAP** sessions given in any particular chapter of this
manual have been run in one continuous session, starting with the two
commands

SizeScreen( [ 72, ] ); LogTo( "erg.log" );

which are used to set the line length to 72 and to save a listing of the session on some file. If you choose any chapter and rerun its examples in the given order, you should be able to reproduce our results except of a few lines of output which we have edited a little bit with respect to blanks or line breaks in order to improve the readability. However, as soon as random processes are involved, you may get different results if you extract single examples and run them separately.

If the program is correctly installed then you start **GAP** by simply
typing `gap`

at the prompt of your operating system followed by the
`return` or the `newline` key.

` $ gap`

**GAP** answers your request with its beautiful banner (which you can
surpress with the command line option `-b`

) and then it shows its own
prompt `gap`

asking you for further input.

` gap>`

The usual way to end a **GAP** session is to type `quit;`

at the `gap`

prompt. Do not omit the semicolon!

gap> quit; $

On some systems you may as well type `ctl`-`D`

to yield the same effect.
In any situation **GAP** is ended by typing `ctl`-`C`

twice within a
second.

A simple calculation with **GAP** is as easy as one can imagine. You type
the problem just after the prompt, terminate it with a semicolon and then
pass the problem to the program with the `return` key. For example, to
multiply the difference between 9 and 7 by the sum of 5 and 6, that is to
calculate *(9 - 7) * (5 + 6)*, you type exactly this last sequence of
symbols followed by `;`

and `return`.

gap> (9 - 7) * (5 + 6); 22 gap>

Then **GAP** echoes the result 22 on the next line and shows with the
prompt that it is ready for the next problem.

If you did omit the semicolon at the end of the line but have already
typed `return`, then **GAP** has read everything you typed, but does not
know that the command is complete. The program is waiting for further
input and indicates this with a partial prompt ```
. This little problem
is solved by simply typing the missing semicolon on the next line of
input. Then the result is printed and the normal prompt returns.
```

gap> (9 - 7) * (5 + 6) > ; 22 gap>

Whenever you see this partial prompt and you cannot decide what **GAP** is
still waiting for, then you have to type semicolons until the normal
prompt returns.

In every situation this is the exact meaning of the prompt `gap`

: the
program is waiting for a new problem. In the following examples we will
omit this prompt on the line after the result. Considering each example
as a continuation of its predecessor this prompt occurs in the next
example.

In this section you have seen how simple arithmetic problems can be
solved by **GAP** by simply typing them in. You have seen that it
doesn't matter whether you complete your input on one line. **GAP**
reads your input line by line and starts evaluating if it has seen the
terminating semicolon and `return`.

It is, however, also possible (and might be advisable for large amounts
of input data) to write your input first into a file, and then read this
into **GAP**; see Edit and Read for this.

Also in **GAP**, there is the possibility to edit the input data, see
Line Editing.

The contents of the **GAP** manual is also available as on-line help, see
Help--Help Index. If you need information about a section of the
manual, just enter a question mark followed by the header of the section.
E.g., entering `?About Help`

will print the section you are reading now.

`??`

will print all entries in `topic`**GAP**'s index that contain the
substring `topic`.

Even if you mistyped the command you do not have to type it all again as
**GAP** permits a lot of command line editing. Maybe you mistyped or
forgot the last closing parenthesis. Then your command is syntactically
incorrect and **GAP** will notice it, incapable of computing the desired
result.

gap> (9 - 7) * (5 + 6; Syntax error: ) expected (9 - 7) * (5 + 6; ^

Instead of the result an error message occurs indicating the place where
an unexpected symbol occurred with an arrow sign `^`

under it. As a
computer program cannot know what your intentions really were, this is
only a hint. But in this case **GAP** is right by claiming that there
should be a closing parenthesis before the semicolon. Now you can type
`ctl`-`P`

to recover the last line of input. It will be written after
the prompt with the cursor in the first position. Type `ctl`-`E`

to take
the cursor to the end of the line, then `ctl`-`B`

to move the cursor one
character back. The cursor is now on the position of the semicolon.
Enter the missing parenthesis by simply typing `)`

. Now the line is
correct and may be passed to **GAP** by hitting the `newline` key. Note
that for this action it is not necessary to move the cursor past the last
character of the input line.

Each line of commands you type is sent to **GAP** for evaluation by
pressing `newline` regardless of the position of the cursor in that line.
We will no longer mention the `newline` key from now on.

Sometimes a syntax error will cause **GAP** to enter a **break loop**. This
is indicated by the special prompt `brk`

. You can leave the break loop
by either typing `return;`

or by hitting `ctl`-`D`

. Then **GAP** will
return to its normal state and show its normal prompt again.

In this section you learned that mistyped input will not lead to big
confusion. If **GAP** detects a syntax error it will print an error
message and return to its normal state. The command line editing allows
you in a comfortable way to manipulate earlier input lines.

The Programming Language. A complete list of command line editing facilities is found in Line Editing. The break loop is described in Break Loops.

In an expression like `(9 - 7) * (5 + 6)`

the constants 5, 6, 7, and 9
are being composed by the operators `+`

, `*`

and `-`

to result in a new
value.

There are three kinds of operators in **GAP**, arithmetical operators,
comparison operators, and logical operators. You have already seen that
it is possible to form the sum, the difference, and the product of two
integer values. There are some more operators applicable to integers in
**GAP**. Of course integers may be divided by each other, possibly
resulting in noninteger rational values.

gap> 12345/25; 2469/5

Note that the numerator and denominator are divided by their greatest common divisor and that the result is uniquely represented as a division instruction.

We haven't met negative numbers yet. So consider the following self--explanatory examples.

gap> -3; 17 - 23; -3 -6

The exponentiation operator is written as `^`

. This operation in
particular might lead to very large numbers. This is no problem for
**GAP** as it can handle numbers of (almost) arbitrary size.

gap> 3^132; 955004950796825236893190701774414011919935138974343129836853841

The `mod`

operator allows you to compute one value modulo another.

gap> 17 mod 3; 2

Note that there must be whitespace around the keyword `mod`

in this
example since `17mod3`

or `17mod`

would be interpreted as identifiers.

**GAP** knows a precedence between operators that may be overridden by
parentheses.

gap> (9 - 7) * 5 = 9 - 7 * 5; false

Besides these arithmetical operators there are comparison operators in
**GAP**. A comparison results in a **boolean value** which is another kind
of constant. Every two objects within **GAP** are comparable via `=`

,
`<>`

, `<`

, `<=`

, `>`

and `>=`

, that is the tests for equality,
inequality, less than, less than or equal, greater than and greater than
or equal. There is an ordering defined on the set of all **GAP** objects
that respects orders on subsets that one might expect. For example the
integers are ordered in the usual way.

gap> 10^5 < 10^4; false

The boolean values `true`

and `false`

can be manipulated via logical
operators, i.~e., the unary operator `not`

and the binary operators `and`

and `or`

. Of course boolean values can be compared, too.

gap> not true; true and false; true or false; false false true gap> 10 > 0 and 10 < 100; true

Another important type of constants in **GAP** are **permutations**. They
are written in cycle notation and they can be multiplied.

gap> (1,2,3); (1,2,3) gap> (1,2,3) * (1,2); (2,3)

The inverse of the permutation `(1,2,3)`

is denoted by `(1,2,3)^-1`

.
Moreover the caret operator `^`

is used to determine the image of a point
under a permutation and to conjugate one permutation by another.

gap> (1,2,3)^-1; (1,3,2) gap> 2^(1,2,3); 3 gap> (1,2,3)^(1,2); (1,3,2)

The last type of constants we want to introduce here are the
**characters**, which are simply objects in **GAP** that represent arbitrary
characters from the character set of the operating system. Character
literals can be entered in **GAP** by enclosing the character in
**singlequotes** `'`

.

gap> 'a'; 'a' gap> '*'; '*'

There are no operators defined for characters except that characters can be compared.

In this section you have seen that values may be preceded by unary
operators and combined by binary operators placed between the operands.
There are rules for precedence which may be overridden by parentheses.
It is possible to compare any two objects. A comparison results in a
boolean value. Boolean values are combined via logical operators.
Moreover you have seen that **GAP** handles numbers of arbitrary size.
Numbers and boolean values are constants. There are other types of
constants in **GAP** like permutations. You are now in a position to use
**GAP** as a simple desktop calculator.

Operators are explained in more detail in Comparisons and Operations. Moreover there are sections about operators and comparisons for special types of objects in almost every chapter of this manual. You will find Boolean Lists. Permutations are described in chapter Permutations and characters are described in chapter Strings and Characters.

Values may be assigned to variables. A variable enables you to refer to
an object via a name. The name of a variable is called an **identifier**.
The assignment operator is `:=`

. There must be no white space between
the `:`

and the `=`

. Do not confuse the assignment operator `:=`

with
the single equality sign `=`

which is in **GAP** only used for the test of
equality.

gap> a:= (9 - 7) * (5 + 6); 22 gap> a; 22 gap> a * (a + 1); 506 gap> a:= 10; 10 gap> a * (a + 1); 110

After an assignment the assigned value is echoed on the next line. The printing of the value of a statement may be in every case prevented by typing a double semicolon.

` gap> w:= 2;;`

After the assignment the variable evaluates to that value if evaluated. Thus it is possible to refer to that value by the name of the variable in any situation.

This is in fact the whole secret of an assignment. An identifier is
bound to a value and from this moment points to that value. Nothing
more. This binding is changed by the next assignment to that identifier.
An identifier does not denote a block of memory as in some other
programming languages. It simply points to a value, which has been given
its place in memory by the **GAP** storage manager. This place may change
during a **GAP** session, but that doesn't bother the identifier.

**The identifier points to the value, not to a place in the memory.**

For the same reason it is not the identifier that has a type but the
object. This means on the other hand that the identifier `a`

which now
is bound to an integer value may in the same session point to any other
value regardless of its type.

Identifiers may be sequences of letters and digits containing at least
one letter. For example `abc`

and `a0bc1`

are valid identifiers. But
also `123a`

is a valid identifier as it cannot be confused with any
number. Just `1234`

indicates the number 1234 and cannot be at the same
time the name of a variable.

Since **GAP** distinguishes upper and lower case, `a1`

and `A1`

are
different identifiers. Keywords such as `quit`

must not be used as
identifiers. You will see more keywords in the following sections.

In the remaining part of this manual we will ignore the difference
between variables, their names (identifiers), and the values they point
at. It may be useful to think from time to time about what is really
meant by terms such as the integer `w`

.

There are some predefined variables coming with **GAP**. Many of them you
will find in the remaining chapters of this manual, since functions are
also referred to via identifiers.

This seems to be the right place to state the following rule.

The name of every function in the **GAP** library starts with a **capital
letter.**

Thus if you choose only names starting with a small letter for your own variables you will not overwrite any predefined function.

But there are some further interesting variables one of which shall be introduced now.

Whenever **GAP** returns a value by printing it on the next line this
value is assigned to the variable `last`

. So if you computed

gap> (9 - 7) * (5 + 6); 22

and forgot to assign the value to the variable `a`

for further use, you
can still do it by the following assignment.

gap> a:= last; 22

Moreover there are variables `last2`

and `last3`

, guess their values.

In this section you have seen how to assign values to variables. These
values can later be accessed through the name of the variable, its
identifier. You have also encountered the useful concept of the `last`

variables storing the latest returned values. And you have learned that
a double semicolon prevents the result of a statement from being printed.

Variables and assignments are described in more detail in Variables and Assignments. A complete list of keywords is contained in Keywords.

A program written in the **GAP** language is called a **function**.
Functions are special **GAP** objects. Most of them behave like
mathematical functions. They are applied to objects and will return a
new object depending on the input. The function `Factorial`

, for
example, can be applied to an integer and will return the factorial of
this integer.

gap> Factorial(17); 355687428096000

Applying a function to arguments means to write the arguments in
parentheses following the function. Several arguments are separated by
commas, as for the function `Gcd`

which computes the greatest common
divisor of two integers.

gap> Gcd(1234, 5678); 2

There are other functions that do not return a value but only produce a
side effect. They change for example one of their arguments. These
functions are sometimes called procedures. The function `Print`

is only
called for the side effect to print something on the screen.

gap> Print(1234, "\n"); 1234

In order to be able to compose arbitrary text with `Print`

, this function
itself will not produce a line break after printing. Thus we had another
newline character `"\n"`

printed to start a new line.

Some functions will both change an argument and return a value such as
the function `Sortex`

that sorts a list and returns the permutation of
the list elements that it has performed.

You will not understand right now what it means to change an object. We will return to this subject several times in the next sections.

A comfortable way to define a function is given by the **maps--to**
operator `-`

consisting of a minus sign and a greater sign with no
whitespace between them. The function `cubed`

which maps a number to its
cube is defined on the following line.

gap> cubed:= x -> x^3; function ( x ) ... end

After the function has been defined, it can now be applied.

gap> cubed(5); 125

Not every **GAP** function can be defined in this way. You will see how
to write your own **GAP** functions in a later section.

In this section you have seen **GAP** objects of type function. You have
learned how to apply a function to arguments. This yields as result a
new object or a side effect. A side effect may change an argument of the
function. Moreover you have seen an easy way to define a function in
**GAP** with the maps-to operator.

Procedure Calls. The functions of the **GAP** library are described in detail in
the remaining chapters of this manual, the Reference Manual.

A **list** is a collection of objects separated by commas and enclosed in
brackets. Let us for example construct the list `primes`

of the first 10
prime numbers.

gap> primes:= [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 ]

The next two primes are 31 and 37. They may be appended to the existing
list by the function `Append`

which takes the existing list as its first
and another list as a second argument. The second argument is appended
to the list `primes`

and no value is returned. Note that by appending
another list the object `primes`

is changed.

gap> Append(primes, [31, 37]); gap> primes; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 ]

You can as well add single new elements to existing lists by the function
`Add`

which takes the existing list as its first argument and a new
element as its second argument. The new element is added to the list
`primes`

and again no value is returned but the list `primes`

is changed.

gap> Add(primes, 41); gap> primes; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 ]

Single elements of a list are referred to by their position in the list.
To get the value of the seventh prime, that is the seventh entry in our
list `primes`

, you simply type

gap> primes[7]; 17

and you will get the value of the seventh prime. This value can be
handled like any other value, for example multiplied by 2 or assigned to
a variable. On the other hand this mechanism allows to assign a value to
a position in a list. So the next prime 43 may be inserted in the list
directly after the last occupied position of `primes`

. This last
occupied position is returned by the function `Length`

.

gap> Length(primes); 13 gap> primes[14]:= 43; 43 gap> primes; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43 ]

Note that this operation again has changed the object `primes`

. Not only
the next position of a list is capable of taking a new value. If you
know that 71 is the 20th prime, you can as well enter it right now in the
20th position of `primes`

. This will result in a list with holes which
is however still a list and has length 20 now.

gap> primes[20]:= 71; 71 gap> primes; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43,,,,,, 71 ] gap> Length(primes); 20

The list itself however must exist before a value can be assigned to a
position of the list. This list may be the empty list `[ ]`

.

gap> lll[1]:= 2; Error, Variable: 'lll' must have a value gap> lll:= []; [ ] gap> lll[1]:= 2; 2

Of course existing entries of a list can be changed by this mechanism,
too. We will not do it here because `primes`

then may no longer be a
list of primes. Try for yourself to change the 17 in the list into a 9.

To get the position of 17 in the list `primes`

use the function
`Position`

which takes the list as its first argument and the element as
its second argument and returns the position of the first occurrence of
the element 17 in the list `primes`

. `Position`

will return `false`

if
the element is not contained in the list.

gap> Position(primes, 17); 7 gap> Position(primes, 20); false

In all of the above changes to the list `primes`

, the list has been
automatically resized. There is no need for you to tell **GAP** how big
you want a list to be. This is all done dynamically.

It is not necessary for the objects collected in a list to be of the same type.

gap> lll:= [true, "This is a String",,, 3]; [ true, "This is a String",,, 3 ]

In the same way a list may be part of another list. A list may even be part of itself.

gap> lll[3]:= [4,5,6];; lll; [ true, "This is a String", [ 4, 5, 6 ],, 3 ] gap> lll[4]:= lll; [ true, "This is a String", [ 4, 5, 6 ], ~, 3 ]

Now the tilde `~`

in the fourth position of `lll`

denotes the object that
is currently printed. Note that the result of the last operation is the
actual value of the object `lll`

on the right hand side of the
assignment. But in fact it is identical to the value of the whole list
`lll`

on the left hand side of the assignment.

A **string** is a very special type of list, which is printed in a
different way. A **string** is simply a dense list of characters. Strings
are used mainly in filenames and error messages. A string literal can
either be entered simply as the list of characters or by writing the
characters between **doublequotes** `"`

. GAP will always output strings
in the latter format.

gap> s1 := ['H','a','l','l','o',' ','w','o','r','l','d','.']; "Hallo world." gap> s2 := "Hallo world."; "Hallo world." gap> s1 := ['H','a','l','l','o',' ','w','o','r','l','d','.']; "Hallo world." gap> s1 = s2; true gap> s2[7]; 'w'

Sublists of lists can easily be extracted and assigned using the operator
`{ }`

.

gap> sl := lll{ [ 1, 2, 3 ] }; [ true, "This is a String", [ 4, 5, 6 ] ] gap> sl{ [ 2, 3 ] } := [ "New String", false ]; [ "New String", false ] gap> sl; [ true, "New String", false ]

This way you get a new list that contains at position `i` that element
whose position is the `i`th entry of the argument of `{ }`

.

In this long section you have encountered the fundamental concept of a list. You have seen how to construct lists, how to extend them and how to refer to single elements of a list. Moreover you have seen that lists may contain elements of different types, even holes (unbound entries). But this is still not all we have to tell you about lists.

You will find a discussion about identity and equality of lists in the next section. Moreover you will see special kinds of lists like sets (in About Sets), vectors and matrices (in About Vectors and Matrices) and Strings and Characters.

This second section about lists is dedicated to the subtle difference
between equality and identity of lists. It is really important to
understand this difference in order to understand how complex data
structures are realized in **GAP**. This section applies to all **GAP**
objects that have subobjects, i.~e., to lists and to records. After
reading the section about records (About Records) you should return to
this section and translate it into the record context.

Two lists are equal if all their entries are equal. This means that the
equality operator `=`

returns `true`

for the comparison of two lists if
and only if these two lists are of the same length and for each position
the values in the respective lists are equal.

gap> numbers:= primes; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43,,,,,, 71 ] gap> numbers = primes; true

We assigned the list `primes`

to the variable `numbers`

and, of course
they are equal as they have both the same length and the same entries.
Now we will change the third number to 4 and compare the result again
with `primes`

.

gap> numbers[3]:= 4; 4 gap> numbers = primes; true

You see that `numbers`

and `primes`

are still equal, check this by
printing the value of `primes`

. The list `primes`

is no longer a list of
primes! What has happened? The truth is that the lists `primes`

and
`numbers`

are not only equal but they are identical. `primes`

and
`numbers`

are two variables pointing to the same list. If you change the
value of the subobject `numbers[3]`

of `numbers`

this will also change
`primes`

. Variables do **not** point to a certain block of storage memory
but they do point to an object that occupies storage memory. So the
assignment `numbers:= primes`

did **not** create a new list in a different
place of memory but only created the new name `numbers`

for the same old
list of primes.

**The same object can have several names.**

If you want to change a list with the contents of `primes`

independently
from `primes`

you will have to make a **copy** of `primes`

by the function
`Copy`

which takes an object as its argument and returns a copy of the
argument. (We will first restore the old value of `primes`

.)

gap> primes[3]:= 5; 5 gap> primes; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43,,,,,, 71 ] gap> numbers:= Copy(primes); [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43,,,,,, 71 ] gap> numbers = primes; true gap> numbers[3]:= 4; 4 gap> numbers = primes; false

Now `numbers`

is no longer equal to `primes`

and `primes`

still is a list
of primes. Check this by printing the values of `numbers`

and `primes`

.

The only objects that can be changed this way are records and lists,
because only **GAP** objects of these types have subobjects. To clarify
this statement consider the following example.

` gap> i:= 1;; j:= i;; i:= i+1;;`

By adding 1 to `i`

the value of `i`

has changed. What happens to `j`

?
After the second statement `j`

points to the same object as `i`

, namely
to the integer 1. The addition does **not** change the object `1`

but
creates a new object according to the instruction `i+1`

. It is actually
the assignment that changes the value of `i`

. Therefore `j`

still points
to the object `1`

. Integers (like permutations and booleans) have no
subobjects. Objects of these types cannot be changed but can only be
replaced by other objects. And a replacement does not change the values
of other variables. In the above example an assignment of a new value to
the variable `numbers`

would also not change the value of `primes`

.

Finally try the following examples and explain the results.

gap> l:= []; [ ] gap> l:= [l]; [ [ ] ] gap> l[1]:= l; [ ~ ]

Now return to the preceding section About Lists and find out whether
the functions `Add`

and `Append`

change their arguments.

In this section you have seen the difference between equal lists and
identical lists. Lists are objects that have subobjects and therefore
can be changed. Changing an object will change the values of all
variables that point to that object. Be careful, since one object can
have several names. The function `Copy`

creates a copy of a list which
is then a new object.

You will find more about lists in chapter Lists, and more about identical lists in Identical Lists.

**GAP** knows several special kinds of lists. A set in **GAP** is a
special kind of list. A set contains no holes and its elements are
sorted according to the **GAP** ordering of all its objects. Moreover a
set contains no object twice.

The function `IsSet`

tests whether an object is a set. It returns a
boolean value. For any list there exists a corresponding set. This set
is constructed by the function `Set`

which takes the list as its argument
and returns a set obtained from this list by ignoring holes and
duplicates and by sorting the elements.

The elements of the sets used in the examples of this section are strings.

gap> fruits:= ["apple", "strawberry", "cherry", "plum"]; [ "apple", "strawberry", "cherry", "plum" ] gap> IsSet(fruits); false gap> fruits:= Set(fruits); [ "apple", "cherry", "plum", "strawberry" ]

Note that the original list `fruits`

is not changed by the function
`Set`

. We have to make a new assignment to the variable `fruits`

in
order to make it a set.

The `in`

operator is used to test whether an object is an element of a
set. It returns a boolean value `true`

or `false`

.

gap> "apple" in fruits; true gap> "banana" in fruits; false

The `in`

operator may as well be applied to ordinary lists. It is
however much faster to perform a membership test for sets since sets are
always sorted and a binary search can be used instead of a linear search.

New elements may be added to a set by the function `AddSet`

which takes
the set `fruits`

as its first argument and an element as its second
argument and adds the element to the set if it wasn't already there.
Note that the object `fruits`

is changed.

gap> AddSet(fruits, "banana"); gap> fruits; # The banana is inserted in the right place. [ "apple", "banana", "cherry", "plum", "strawberry" ] gap> AddSet(fruits, "apple"); gap> fruits; # 'fruits' has not changed. [ "apple", "banana", "cherry", "plum", "strawberry" ]

Sets can be intersected by the function `Intersection`

and united by the
function `Union`

which both take two sets as their arguments and return
the intersection (union) of the two sets as a new object.

gap> breakfast:= ["tea", "apple", "egg"]; [ "tea", "apple", "egg" ] gap> Intersection(breakfast, fruits); [ "apple" ]

It is however not necessary for the objects collected in a set to be of
the same type. You may as well have additional integers and boolean
values for `breakfast`

.

The arguments of the functions `Intersection`

and `Union`

may as well be
ordinary lists, while their result is always a set. Note that in the
preceding example at least one argument of `Intersection`

was not a set.

The functions `IntersectSet`

and `UniteSet`

also form the intersection
resp.~union of two sets. They will however not return the result but
change their first argument to be the result. Try them carefully.

In this section you have seen that sets are a special kind of list.
There are functions to expand sets, intersect or unite sets, and there is
the membership test with the `in`

operator.

Strings and Characters. Sets are described in more detail in chapter Sets.

A **vector** is a list of elements from a common field. A **matrix** is a
list of vectors of equal length. Vectors and matrices are special kinds
of lists without holes.

gap> v:= [3, 6, 2, 5/2]; [ 3, 6, 2, 5/2 ] gap> IsVector(v); true

Vectors may be multiplied by scalars from their field. Multiplication of vectors of equal length results in their scalar product.

gap> 2 * v; [ 6, 12, 4, 5 ] gap> v * 1/3; [ 1, 2, 2/3, 5/6 ] gap> v * v; 221/4 # the scalar product of 'v' with itself

Note that the expression `v * 1/3`

is actually evaluated by first
multiplying `v`

by 1 (which yields again `v`

) and by then dividing by 3.
This is also an allowed scalar operation. The expression `v/3`

would
result in the same value.

A matrix is a list of vectors of equal length.

gap> m:= [[1, -1, 1], > [2, 0, -1], > [1, 1, 1]]; [ [ 1, -1, 1 ], [ 2, 0, -1 ], [ 1, 1, 1 ] ] gap> m[2][1]; 2

Syntactically a matrix is a list of lists. So the number 2 in the second
row and the first column of the matrix `m`

is referred to as the first
element of the second element of the list `m`

via `m[2][1]`

.

A matrix may be multiplied by scalars, vectors and other matrices. The vectors and matrices involved in such a multiplication must however have suitable dimensions.

gap> m:= [[1, 2, 3, 4], > [5, 6, 7, 8], > [9,10,11,12]]; [ [ 1, 2, 3, 4 ], [ 5, 6, 7, 8 ], [ 9, 10, 11, 12 ] ] gap> PrintArray(m); [ [ 1, 2, 3, 4 ], [ 5, 6, 7, 8 ], [ 9, 10, 11, 12 ] ] gap> [1, 0, 0, 0] * m; Error, Vector *: vectors must have the same length gap> [1, 0, 0] * m; [ 1, 2, 3, 4 ] gap> m * [1, 0, 0]; Error, Vector *: vectors must have the same length gap> m * [1, 0, 0, 0]; [ 1, 5, 9 ] gap> m * [0, 1, 0, 0]; [ 2, 6, 10 ]

Note that multiplication of a vector with a matrix will result in a linear combination of the rows of the matrix, while multiplication of a matrix with a vector results in a linear combination of the columns of the matrix. In the latter case the vector is considered as a column vector.

Submatrices can easily be extracted and assigned using the `{ }{ }`

operator.

gap> sm := m{ [ 1, 2 ] }{ [ 3, 4 ] }; [ [ 3, 4 ], [ 7, 8 ] ] gap> sm{ [ 1, 2 ] }{ [2] } := [[1],[-1]]; [ [ 1 ], [ -1 ] ] gap> sm; [ [ 3, 1 ], [ 7, -1 ] ]

The first curly brackets contain the selection of rows, the second that of columns.

In this section you have met vectors and matrices as special lists. You have seen how to refer to elements of a matrix and how to multiply scalars, vectors, and matrices.

Fields are described in chapter Fields. The known fields in **GAP** are
Subfields of Cyclotomic Fields and Finite Fields. Vectors and matrices are
described in more detail in chapters Vectors and Matrices. Vector
spaces are described in chapter Vector Spaces and further matrix
Matrix Groups.

A record provides another way to build new data structures. Like a list
a record is a collection of other objects. In a record the elements are
not indexed by numbers but by names (i.e., identifiers). An entry in a
record is called a **record component** (or sometimes also record field).

gap> date:= rec(year:= 1992, > month:= "Jan", > day:= 13); rec( year := 1992, month := "Jan", day := 13 )

Initially a record is defined as a comma separated list of assignments to its record components. Then the value of a record component is accessible by the record name and the record component name separated by one dot as the record component selector.

gap> date.year; 1992 gap> date.time:= rec(hour:= 19, minute:= 23, second:= 12); rec( hour := 19, minute := 23, second := 12 ) gap> date; rec( year := 1992, month := "Jan", day := 13, time := rec( hour := 19, minute := 23, second := 12 ) )

Assignments to new record components are possible in the same way. The record is automatically resized to hold the new component.

Most of the complex structures that are handled by **GAP** are represented
as records, for instance groups and character tables.

Records are objects that may be changed. An assignment to a record
component changes the original object. There are many functions in the
library that will do such assignments to a record component of one of
their arguments. The function `Size`

for example, will compute the size
of its argument which may be a group for instance, and then store the
value in the record component `size`

. The next call of `Size`

for this
object will use this stored value rather than compute it again.

Lists and records are the **only** types of **GAP** objects that can be
changed.

Sometimes it is interesting to know which components of a certain record
are bound. This information is available from the function `RecFields`

(yes, this function should be called `RecComponentNames`

), which takes a
record as its argument and returns a list of all bound components of this
record as a list of strings.

gap> RecFields(date); [ "year", "month", "day", "time" ]

Finally try the following examples and explain the results.

gap> r:= rec(); rec( ) gap> r:= rec(r:= r); rec( r := rec( ) ) gap> r.r:= r; rec( r := ~ )

Now return to section About Identical Lists and find out what that section means for records.

In this section you have seen how to define and how to use records. Record objects are changed by assignments to record fields. Lists and records are the only types of objects that can be changed.

Records and functions for records are described in detail in chapter Records. More about identical records is found in Identical Records.

A **range** is a finite sequence of integers. This is another special kind
of list. A range is described by its minimum (the first entry), its
second entry and its maximum, separated by a comma resp. two dots and
enclosed in brackets. In the usual case of an ascending list of
consecutive integers the second entry may be omitted.

gap> [1..999999]; # a range of almost a million numbers [ 1 .. 999999 ] gap> [1, 2..999999]; # this is equivalent [ 1 .. 999999 ] gap> [1, 3..999999]; # here the step is 2 [ 1, 3 .. 999999 ] gap> Length( last ); 500000 gap> [ 999999, 999997 .. 1 ]; [ 999999, 999997 .. 1 ]

This compact printed representation of a fairly long list corresponds to
a compact internal representation. The function `IsRange`

tests whether
an object is a range. If this is true for a list but the list is not yet
represented in the compact form of a range this will be done then.

gap> a:= [-2,-1,0,1,2,3,4,5]; [ -2, -1, 0, 1, 2, 3, 4, 5 ] gap> IsRange(a); true gap> a; [ -2 .. 5 ] gap> a[5]; 2 gap> Length(a); 8

Note that this change of representation does **not** change the value of
the list `a`

. The list `a`

still behaves in any context in the same way
as it would have in the long representation.

In this section you have seen that ascending lists of consecutive integers can be represented in a compact way as ranges.

Chapter Ranges contains a detailed description of ranges. A fundamental application of ranges is introduced in the next section.

Given a list `pp`

of permutations we can form their product by means of a
`for`

loop instead of writing down the product explicitly.

gap> pp:= [ (1,3,2,6,8)(4,5,9), (1,6)(2,7,8)(4,9), (1,5,7)(2,3,8,6), > (1,8,9)(2,3,5,6,4), (1,9,8,6,3,4,7,2) ];; gap> prod:= (); () gap> for p in pp do > prod:= prod * p; > od; gap> prod; (1,8,4,2,3,6,5)

First a new variable `prod`

is initialized to the identity permutation
`()`

. Then the loop variable `p`

takes as its value one permutation
after the other from the list `pp`

and is multiplied with the present
value of `prod`

resulting in a new value which is then assigned to
`prod`

.

The `for`

loop has the following syntax.

`for `

`var` in `list` do `statements` od;

The effect of the `for`

loop is to execute the `statements` for every
element of the `list`. A `for`

loop is a statement and therefore
terminated by a semicolon. The list of `statements` is enclosed by the
keywords `do`

and `od`

(reverse `do`

). A `for`

loop returns no value.
Therefore we had to ask explicitly for the value of `prod`

in the
preceding example.

The `for`

loop can loop over any kind of list, even a list with holes.
In many programming languages (and in former versions of **GAP**, too)
the `for`

loop has the form

`for `

`var` from `first` to `last` do `statements` od;

But this is merely a special case of the general `for`

loop as defined
above where the `list` in the loop body is a range.

`for `

`var` in [`first`..`last`] do `statements` od;

You can for instance loop over a range to compute the factorial *15!* of
the number 15 in the following way.

gap> ff:= 1; 1 gap> for i in [1..15] do > ff:= ff * i; > od; gap> ff; 1307674368000

The following example introduces the `while`

loop which has the following
syntax.

`while `

`condition` do `statements` od;

The `while`

loop loops over the `statements` as long as the `condition`
evaluates to `true`

. Like the `for`

loop the `while`

loop is terminated
by the keyword `od`

followed by a semicolon.

We can use our list `primes`

to perform a very simple factorization. We
begin by initializing a list `factors`

to the empty list. In this list
we want to collect the prime factors of the number 1333. Remember that a
list has to exist before any values can be assigned to positions of the
list. Then we will loop over the list `primes`

and test for each prime
whether it divides the number. If it does we will divide the number by
that prime, add it to the list `factors`

and continue.

gap> n:= 1333; 1333 gap> factors:= []; [ ] gap> for p in primes do > while n mod p = 0 do > n:= n/p; > Add(factors, p); > od; > od; gap> factors; [ 31, 43 ] gap> n; 1

As `n`

now has the value 1 all prime factors of 1333 have been found and
`factors`

contains a complete factorization of 1333. This can of course
be verified by multiplying 31 and 43.

This loop may be applied to arbitrary numbers in order to find prime
factors. But as `primes`

is not a complete list of all primes this loop
may fail to find all prime factors of a number greater than 2000, say.
You can try to improve it in such a way that new primes are added to the
list `primes`

if needed.

You have already seen that list objects may be changed. This holds of
course also for the list in a loop body. In most cases you have to be
careful not to change this list, but there are situations where this is
quite useful. The following example shows a quick way to determine the
primes smaller than 1000 by a sieve method. Here we will make use of the
function `Unbind`

to delete entries from a list.

gap> primes:= []; [ ] gap> numbers:= [2..1000]; [ 2 .. 1000 ] gap> for p in numbers do > Add(primes, p); > for n in numbers do > if n mod p = 0 then > Unbind(numbers[n-1]); > fi; > od; > od;

The inner loop removes all entries from `numbers`

that are divisible by
the last detected prime `p`

. This is done by the function `Unbind`

which
deletes the binding of the list position `numbers[n-1]`

to the value `n`

so that afterwards `numbers[n-1]`

no longer has an assigned value. The
next element encountered in `numbers`

by the outer loop necessarily is
the next prime.

In a similar way it is possible to enlarge the list which is looped over. This yields a nice and short orbit algorithm for the action of a group, for example.

In this section you have learned how to loop over a list by the `for`

loop and how to loop with respect to a logical condition with the `while`

loop. You have seen that even the list in the loop body can be changed.

The `for`

loop is described in For. The `while`

loop is described in
While.

There is however a more comfortable way to compute the product of a list of numbers or permutations.

gap> Product([1..15]); 1307674368000 gap> Product(pp); (1,8,4,2,3,6,5)

The function `Product`

takes a list as its argument and computes the
product of the elements of the list. This is possible whenever a
multiplication of the elements of the list is defined. So `Product`

is
just an implementation of the loop in the example above as a function.

There are other often used loops available as functions. Guess what the
function `Sum`

does. The function `List`

may take a list and a function
as its arguments. It will then apply the function to each element of the
list and return the corresponding list of results. A list of cubes is
produced as follows with the function `cubed`

from About Functions.

gap> List([2..10], cubed); [ 8, 27, 64, 125, 216, 343, 512, 729, 1000 ]

To add all these cubes we might apply the function `Sum`

to the last
list. But we may as well give the function `cubed`

to `Sum`

as an
additional argument.

gap> Sum(last) = Sum([2..10], cubed); true

The primes less than 30 can be retrieved out of the list `primes`

from
section About Lists by the function `Filtered`

. This function takes
the list `primes`

and a property as its arguments and will return the
list of those elements of `primes`

which have this property. Such a
property will be represented by a function that returns a boolean value.
In this example the property of being less than 30 can be reresented by
the function `x- x < 30`

since `x < 30`

will evaluate to `true`

for
values `x`

less than 30 and to `false`

otherwise.

gap> Filtered(primes, x-> x < 30); [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 ]

Another useful thing is the operator `{ }`

that forms sublists. It takes
a list of positions as its argument and will return the list of elements
from the original list corresponding to these positions.

gap> primes{ [1 .. 10] }; [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 ]

In this section you have seen some functions which implement often used
`for`

loops. There are functions like `Product`

to form the product of
the elements of a list. The function `List`

can apply a function to all
elements of a list and the functions `Filtered`

and `Sublist`

create
sublists of a given list.

You will find more predefined `for`

loops in chapter Lists.

You have already seen how to use the functions of the **GAP** library,
i.e., how to apply them to arguments. This section will show you how to
write your own functions.

Writing a function that prints `hello, world.`

on the screen is a simple
exercise in **GAP**.

gap> sayhello:= function() > Print("hello, world.\n"); > end; function ( ) ... end

This function when called will only execute the `Print`

statement in the
second line. This will print the string `hello, world.`

on the screen
followed by a newline character `\n`

that causes the **GAP** prompt to
appear on the next line rather than immediately following the printed
characters.

The function definition has the following syntax.

`function(`

`arguments`) `statements` end

A function definition starts with the keyword `function`

followed by the
formal parameter list `arguments` enclosed in parenthesis. The formal
parameter list may be empty as in the example. Several parameters are
separated by commas. Note that there must be **no** semicolon behind the
closing parenthesis. The function definition is terminated by the
keyword `end`

.

A **GAP** function is an expression like integers, sums and lists. It
therefore may be assigned to a variable. The terminating semicolon in
the example does not belong to the function definition but terminates the
assignment of the function to the name `sayhello`

. Unlike in the case of
integers, sums, and lists the value of the function `sayhello`

is echoed
in the abbreviated fashion `function ( ) ... end`

. This shows the most
interesting part of a function: its formal parameter list (which is
empty in this example). The complete value of `sayhello`

is returned if
you use the function `Print`

.

gap> Print(sayhello, "\n"); function ( ) Print( "hello, world.\n" ); end

Note the additional newline character `"\n"`

in the `Print`

statement.
It is printed after the object `sayhello`

to start a new line.

The newly defined function `sayhello`

is executed by calling `sayhello()`

with an empty argument list.

gap> sayhello(); hello, world.

This is however not a typical example as no value is returned but only a string is printed.

A more useful function is given in the following example. We define a
function `sign`

which shall determine the sign of a number.

gap> sign:= function(n) > if n < 0 then > return -1; > elif n = 0 then > return 0; > else > return 1; > fi; > end; function ( n ) ... end gap> sign(0); sign(-99); sign(11); 0 -1 1 gap> sign("abc"); 1 # strings are defined to be greater than 0

This example also introduces the `if`

statement which is used to execute
statements depending on a condition. The `if`

statement has the
following syntax.

`if `

`condition` then `statements` elif `condition` then `statements` else
`statements` fi;

There may be several `elif`

parts. The `elif`

part as well as the `else`

part of the `if`

statement may be omitted. An `if`

statement is no
expression and can therefore not be assigned to a variable. Furthermore
an `if`

statement does not return a value.

Fibonacci numbers are defined recursively by *f(1) = f(2) = 1* and *f(n)
= f(n-1) + f(n-2)*. Since functions in **GAP** may call themselves, a
function `fib`

that computes Fibonacci numbers can be implemented
basically by typing the above equations.

gap> fib:= function(n) > if n in [1, 2] then > return 1; > else > return fib(n-1) + fib(n-2); > fi; > end; function ( n ) ... end gap> fib(15); 610

There should be additional tests for the argument `n`

being a positive
integer. This function `fib`

might lead to strange results if called
with other arguments. Try to insert the tests in this example.

A function `gcd`

that computes the greatest common divisor of two
integers by Euclid's algorithm will need a variable in addition to the
formal arguments.

gap> gcd:= function(a, b) > local c; > while b <> 0 do > c:= b; > b:= a mod b; > a:= c; > od; > return c; > end; function ( a, b ) ... end gap> gcd(30, 63); 3

The additional variable `c`

is declared as a **local** variable in the
`local`

statement of the function definition. The `local`

statement, if
present, must be the first statement of a function definition. When
several local variables are declared in only one `local`

statement they
are separated by commas.

The variable `c`

is indeed a local variable, that is local to the
function `gcd`

. If you try to use the value of `c`

in the main loop you
will see that `c`

has no assigned value unless you have already assigned
a value to the variable `c`

in the main loop. In this case the local
nature of `c`

in the function `gcd`

prevents the value of the `c`

in the
main loop from being overwritten.

We say that in a given scope an identifier identifies a unique variable.
A **scope** is a lexical part of a program text. There is the global scope
that encloses the entire program text, and there are local scopes that
range from the `function`

keyword, denoting the beginning of a function
definition, to the corresponding `end`

keyword. A local scope introduces
new variables, whose identifiers are given in the formal argument list
and the local declaration of the function. The usage of an identifier in
a program text refers to the variable in the innermost scope that has
this identifier as its name.

We will now write a function to determine the number of partitions of a
positive integer. A partition of a positive integer is a descending list
of numbers whose sum is the given integer. For example *[4,2,1,1]* is a
partition of 8. The complete set of all partitions of an integer *n* may
be divided into subsets with respect to the largest element. The number
of partitions of *n* therefore equals the sum of the numbers of
partitions of *n-i* with elements less than *i* for all possible *i*.
More generally the number of partitions of *n* with elements less than
*m* is the sum of the numbers of partitions of *n-i* with elements less
than *i* for *i* less than *m* and *n*. This description yields the
following function.

gap> nrparts:= function(n) > local np; > np:= function(n, m) > local i, res; > if n = 0 then > return 1; > fi; > res:= 0; > for i in [1..Minimum(n,m)] do > res:= res + np(n-i, i); > od; > return res; > end; > return np(n,n); > end; function ( n ) ... end

We wanted to write a function that takes one argument. We solved the
problem of determining the number of partitions in terms of a recursive
procedure with two arguments. So we had to write in fact two functions.
The function `nrparts`

that can be used to compute the number of
partitions takes indeed only one argument. The function `np`

takes two
arguments and solves the problem in the indicated way. The only task of
the function `nrparts`

is to call `np`

with two equal arguments.

We made `np`

local to `nrparts`

. This illustrates the possibility of
having local functions in **GAP**. It is however not necessary to put it
there. `np`

could as well be defined on the main level. But then the
identifier `np`

would be bound and could not be used for other purposes.
And if it were used the essential function `np`

would no longer be
available for `nrparts`

.

Now have a look at the function `np`

. It has two local variables `res`

and `i`

. The variable `res`

is used to collect the sum and `i`

is a loop
variable. In the loop the function `np`

calls itself again with other
arguments. It would be very disturbing if this call of `np`

would use
the same `i`

and `res`

as the calling `np`

. Since the new call of `np`

creates a new scope with new variables this is fortunately not the case.

The formal parameters *n* and *m* are treated like local variables.

It is however cheaper (in terms of computing time) to avoid such a recursive solution if this is possible (and it is possible in this case), because a function call is not very cheap.

In this section you have seen how to write functions in the **GAP**
language. You have also seen how to use the `if`

statement. Functions
may have local variables which are declared in an initial `local`

statement in the function definition. Functions may call themselves.

The function syntax is described in Functions. The `if`

statement is
described in more detail in If. More about Fibonacci numbers is found
in Fibonacci and more about partitions in Partitions.

In this section we will show some easy computations with groups. The
example uses permutation groups, but this is visible for the user only
because the output contains permutations. The functions, like `Group`

,
`Size`

or `SylowSubgroup`

(for detailed information, see chapters
Domains, Groups), are the same for all kinds of groups, although the
algorithms which compute the information of course will be different in
most cases.

It is not even necessary to know more about permutations than the two facts that they are elements of permutation groups and that they are written in disjoint cycle notation (see chapter Permutations). So let's construct a permutation group:

gap> s8:= Group( (1,2), (1,2,3,4,5,6,7,8) ); Group( (1,2), (1,2,3,4,5,6,7,8) )

We formed the group generated by the permutations `(1,2)`

and
`(1,2,3,4,5,6,7,8)`

, which is well known as the symmetric group on eight
points, and assigned it to the identifier `s8`

. `s8`

contains the
alternating group on eight points which can be described in several ways,
e.g., as group of all even permutations in `s8`

, or as its commutator
subgroup.

gap> a8:= CommutatorSubgroup( s8, s8 ); Subgroup( Group( (1,2), (1,2,3,4,5,6,7,8) ), [ (1,3,2), (2,4,3), (2,3)(4,5), (2,4,6,5,3), (2,5,3)(4,7,6), (2,3)(5,6,8,7) ] )

The alternating group `a8`

is printed as instruction to compute that
subgroup of the group `s8`

that is generated by the given six
permutations. This representation is much shorter than the internal
structure, and it is completely self--explanatory; one could, for
example, print such a group to a file and read it into **GAP** later. But
if one object occurs several times it is useful to refer to this object;
this can be settled by assigning a name to the group.

gap> s8.name:= "s8"; "s8" gap> a8; Subgroup( s8, [ (1,3,2), (2,4,3), (2,3)(4,5), (2,4,6,5,3), (2,5,3)(4,7,6), (2,3)(5,6,8,7) ] ) gap> a8.name:= "a8"; "a8" gap> a8; a8

Whenever a group has a component `name`

, **GAP** prints this name instead
of the group itself. Note that there is no link between the name and the
identifier, but it is of course useful to choose name and identifier
compatible.

gap> copya8:= Copy( a8 ); a8

We examine the group `a8`

. Like all complex **GAP** structures, it is
represented as a record (see Group Records).

gap> RecFields( a8 ); [ "isDomain", "isGroup", "parent", "identity", "generators", "operations", "isPermGroup", "1", "2", "3", "4", "5", "6", "stabChainOptions", "stabChain", "orbit", "transversal", "stabilizer", "name" ]

Many functions store information about the group in this group record, this avoids duplicate computations. But we are not interested in the organisation of data but in the group, e.g., some of its properties (see chapter Groups, especially Properties and Property Tests):

gap> Size( a8 ); IsAbelian( a8 ); IsPerfect( a8 ); 20160 false true

Some interesting subgroups are the Sylow *p* subgroups for prime divisors
*p* of the group order; a call of `SylowSubgroup`

stores the required
subgroup in the group record:

gap> Set( Factors( Size( a8 ) ) ); [ 2, 3, 5, 7 ] gap> for p in last do > SylowSubgroup( a8, p ); > od; gap> a8.sylowSubgroups; [ , Subgroup( s8, [ (1,5)(7,8), (1,5)(2,6), (3,4)(7,8), (2,3)(4,6), (1,7)(2,3)(4,6)(5,8), (1,2)(3,7)(4,8)(5,6) ] ), Subgroup( s8, [ (3,8,7), (2,6,4)(3,7,8) ] ),, Subgroup( s8, [ (3,7,8,6,4) ] ),, Subgroup( s8, [ (2,8,4,5,7,3,6) ] ) ]

The record component `sylowSubgroups`

is a list which stores at the
*p*--th position, if bound, the Sylow *p* subgroup; in this example this
means that there are holes at positions 1, 4 and 6. Note that a call of
`SylowSubgroup`

for the cyclic group of order 65521 and for the prime
65521 would cause **GAP** to store the group at the end of a list of
length 65521, so there are special situations where it is possible to
bring **GAP** and yourselves into troubles.

We now can investigate the Sylow 2 subgroup.

gap> syl2:= last[2];; gap> Size( syl2 ); 64 gap> Normalizer( a8, syl2 ); Subgroup( s8, [ (3,4)(7,8), (2,3)(4,6), (1,2)(3,7)(4,8)(5,6) ] ) gap> last = syl2; true gap> Centre( syl2 ); Subgroup( s8, [ ( 1, 5)( 2, 6)( 3, 4)( 7, 8) ] ) gap> cent:= Centralizer( a8, last ); Subgroup( s8, [ ( 1, 5)( 2, 6)( 3, 4)( 7, 8), (3,4)(7,8), (3,7)(4,8), (2,3)(4,6), (1,2)(5,6) ] ) gap> Size( cent ); 192 gap> DerivedSeries( cent ); [ Subgroup( s8, [ ( 1, 5)( 2, 6)( 3, 4)( 7, 8), (3,4)(7,8), (3,7)(4,8), (2,3)(4,6), (1,2)(5,6) ] ), Subgroup( s8, [ ( 1, 6, 3)( 2, 4, 5), ( 1, 8, 3)( 4, 5, 7), ( 1, 7)( 2, 3)( 4, 6)( 5, 8), ( 1, 5)( 2, 6) ] ), Subgroup( s8, [ ( 1, 3)( 2, 7)( 4, 5)( 6, 8), ( 1, 6)( 2, 5)( 3, 8)( 4, 7), ( 1, 5)( 3, 4), ( 1, 5)( 7, 8) ] ) , Subgroup( s8, [ ( 1, 5)( 2, 6)( 3, 4)( 7, 8) ] ), Subgroup( s8, [ ] ) ] gap> List( last, Size ); [ 192, 96, 32, 2, 1 ] gap> low:= LowerCentralSeries( cent ); [ Subgroup( s8, [ ( 1, 5)( 2, 6)( 3, 4)( 7, 8), (3,4)(7,8), (3,7)(4,8), (2,3)(4,6), (1,2)(5,6) ] ), Subgroup( s8, [ ( 1, 6, 3)( 2, 4, 5), ( 1, 8, 3)( 4, 5, 7), ( 1, 7)( 2, 3)( 4, 6)( 5, 8), ( 1, 5)( 2, 6) ] ) ]

Another kind of subgroups is given by the point stabilizers.

gap> stab:= Stabilizer( a8, 1 ); Subgroup( s8, [ (2,5,6), (2,5)(3,6), (2,5,6,4,3), (2,5,3)(4,6,8), (2,5)(3,4,7,8) ] ) gap> Size( stab ); 2520 gap> Index( a8, stab ); 8

We can fetch an arbitrary group element and look at its centralizer in
`a8`

, and then get other subgroups by conjugation and intersection of
already known subgroups. Note that we form the subgroups inside `a8`

,
but **GAP** regards these groups as subgroups of `s8`

because this is the
common ``parent'' group of all these groups and of `a8`

(for the idea
of parent groups, see More about Groups and Subgroups).

gap> Random( a8 ); (1,6,3,2,7)(4,5,8) gap> Random( a8 ); (1,3,2,4,7,5,6) gap> cent:= Centralizer( a8, (1,2)(3,4)(5,8)(6,7) ); Subgroup( s8, [ (1,2)(3,4)(5,8)(6,7), (5,6)(7,8), (5,7)(6,8), (3,4)(6,7), (3,5)(4,8), (1,3)(2,4) ] ) gap> Size( cent ); 192 gap> conj:= ConjugateSubgroup( cent, (2,3,4) ); Subgroup( s8, [ (1,3)(2,4)(5,8)(6,7), (5,6)(7,8), (5,7)(6,8), (2,4)(6,7), (2,8)(4,5), (1,4)(2,3) ] ) gap> inter:= Intersection( cent, conj ); Subgroup( s8, [ (5,6)(7,8), (5,7)(6,8), (1,2)(3,4), (1,3)(2,4) ] ) gap> Size( inter ); 16 gap> IsElementaryAbelian( inter ); true gap> norm:= Normalizer( a8, inter ); Subgroup( s8, [ (6,7,8), (5,6,8), (3,4)(6,8), (2,3)(6,8), (1,2)(6,8), (1,5)(2,6,3,7,4,8) ] ) gap> Size( norm ); 576

Suppose we do not only look which funny things may appear in our group
but want to construct a subgroup, e.g., a group of structure
*2^3:L_3(2)* in `a8`

. One idea is to look for an appropriate *2^3*
which is specified by the fact that all its involutions are fixed point
free, and then compute its normalizer in `a8`

:

gap> elab:= Group( (1,2)(3,4)(5,6)(7,8), (1,3)(2,4)(5,7)(6,8), > (1,5)(2,6)(3,7)(4,8) );; gap> Size( elab ); 8 gap> IsElementaryAbelian( elab ); true gap> norm:= Normalizer( a8, AsSubgroup( s8, elab ) ); Subgroup( s8, [ (5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (2,3)(6,7), (1,2)(7,8) ] ) gap> Size( norm ); 1344

Note that `elab`

was defined as separate group, thus we had to call
`AsSubgroup`

to achieve that it has the same parent group as `a8`

.
Let's look at some usual misuses:

` Normalizer( a8, elab );`

Intuitively, it is clear that here again we wanted to compute the
normalizer of `elab`

in `a8`

, and in fact we would get it by this call.
However, this would be a misuse in the sense that now **GAP** cannot use
some clever method for the computation of the normalizer. So, for larger
groups, the computation may be very time consuming. That is the reason
why we used the the function `AsSubgroup`

in the preceding example.

Let's have a closer look at that function.

gap> IsSubgroup( a8, AsSubgroup( a8, elab ) ); Error, <G> must be a parent group in AsSubgroup( a8, elab ) called from main loop brk> quit; gap> IsSubgroup( a8, AsSubgroup( s8, elab ) ); true

What we tried here was not correct. Since all our computations up to now
are done inside `s8`

which is the parent of `a8`

, it is easy to
understand that `IsSubgroup`

works for two subgroups with this parent.

By the way, you should not try the operator `<`

instead of the function
`IsSubgroup`

. Something like

gap> elab < a8; false

or

gap> AsSubgroup( s8, elab ) < a8; false

will not cause an error, but the result does not tell anything about the
inclusion of one group in another; `<`

looks at the element lists for
the two domains which means that it computes them if they are not already
stored --which is not desirable to do for large groups-- and then simply
compares the lists with respect to lexicographical order (see
Comparisons of Domains).

On the other hand, the equality operator `=`

in fact does test the
equality of groups. Thus

gap> elab = AsSubgroup( s8, elab ); true

means that the two groups are equal in the sense that they have the same
elements. Note that they may behave differently since they have
different parent groups. In our example, it is necessary to work with
subgroups of `s8`

:

gap> elab:= AsSubgroup( s8, elab );; gap> elab.name:= "elab";;

If we are given the subgroup `norm`

of order 1344 and its subgroup
`elab`

, the factor group can be considered.

gap> f:= norm / elab; (Subgroup( s8, [ (5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (2,3)(6,7), (1,2)(7,8) ] ) / elab) gap> Size( f ); 168

As the output shows, this is not a permutation group. The factor group and its elements can, however, be handled in the usual way.

gap> Random( f ); FactorGroupElement( elab, (2,8,7)(3,5,6) ) gap> Order( f, last ); 3

The natural link between the group `norm`

and its factor group `f`

is the
natural homomorphism onto `f`

, mapping each element of `norm`

to its
coset modulo the kernel `elab`

. In **GAP** you can construct the
homomorphism, but note that the images lie in `f`

since they are elements
of the factor group, but the preimage of each such element is only a
coset, not a group element (for cosets, see the relevant sections in
chapter Groups, for homomorphisms see chapters Operations of Groups
and Mappings).

gap> f.name:= "f";; gap> hom:= NaturalHomomorphism( norm, f ); NaturalHomomorphism( Subgroup( s8, [ (5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (2,3)(6,7), (1,2)(7,8) ] ), (Subgroup( s8, [ (5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (2,3)(6,7), (1,2)(7,8) ] ) / elab) ) gap> Kernel( hom ) = elab; true gap> x:= Random( norm ); (1,7,5,8,3,6,2) gap> Image( hom, x ); FactorGroupElement( elab, (2,7,3,4,6,8,5) ) gap> coset:= PreImages( hom, last ); (elab*(2,7,3,4,6,8,5)) gap> IsCoset( coset ); true gap> x in coset; true gap> coset in f; false

The group `f`

acts on its elements (**not** on the cosets) via right
multiplication, yielding the regular permutation representation of `f`

and thus a new permutation group, namely the linear group *L_3(2)*. A
more elaborate discussion of operations of groups can be found in section
About Operations of Groups and chapter Operations of Groups.

gap> op:= Operation( f, Elements( f ), OnRight );; gap> IsPermGroup( op ); true gap> Maximum( List( op.generators, LargestMovedPointPerm ) ); 168 gap> IsSimple( op ); true

`norm`

acts on the seven nontrivial elements of its normal subgroup
`elab`

by conjugation, yielding a representation of *L_3(2)* on seven
points. We embed this permutation group in `norm`

and deduce that `norm`

is a split extension of an elementary abelian group *2^3* with *L_3(2)*.

gap> op:= Operation( norm, Elements( elab ), OnPoints ); Group( (5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (2,3)(6,7), (3,4)(5,6) ) gap> IsSubgroup( a8, AsSubgroup( s8, op ) ); true gap> IsSubgroup( norm, AsSubgroup( s8, op ) ); true gap> Intersection( elab, op ); Group( () )

Yet another kind of information about our `a8`

concerns its conjugacy
classes.

gap> ccl:= ConjugacyClasses( a8 ); [ ConjugacyClass( a8, () ), ConjugacyClass( a8, (1,3)(2,6)(4,7)(5,8) ) , ConjugacyClass( a8, (1,3)(2,8,5)(6,7) ), ConjugacyClass( a8, (2,5,8) ), ConjugacyClass( a8, (1,3)(6,7) ), ConjugacyClass( a8, (1,3,2,5,4,7,8) ), ConjugacyClass( a8, (1,5,8,2,7,3,4) ), ConjugacyClass( a8, (1,5)(2,8,7,4,3,6) ), ConjugacyClass( a8, (2,7,3)(4,6,8) ), ConjugacyClass( a8, (1,6)(3,8,5,4) ), ConjugacyClass( a8, (1,3,5,2)(4,6,8,7) ), ConjugacyClass( a8, (1,8,6,2,5) ), ConjugacyClass( a8, (1,7,2,4,3)(5,8,6) ), ConjugacyClass( a8, (1,2,3,7,4)(5,8,6) ) ] gap> Length( ccl ); 14 gap> reps:= List( ccl, Representative ); [ (), (1,3)(2,6)(4,7)(5,8), (1,3)(2,8,5)(6,7), (2,5,8), (1,3)(6,7), (1,3,2,5,4,7,8), (1,5,8,2,7,3,4), (1,5)(2,8,7,4,3,6), (2,7,3)(4,6,8), (1,6)(3,8,5,4), (1,3,5,2)(4,6,8,7), (1,8,6,2,5), (1,7,2,4,3)(5,8,6), (1,2,3,7,4)(5,8,6) ] gap> List( reps, r -> Order( a8, r ) ); [ 1, 2, 6, 3, 2, 7, 7, 6, 3, 4, 4, 5, 15, 15 ] gap> List( ccl, Size ); [ 1, 105, 1680, 112, 210, 2880, 2880, 3360, 1120, 2520, 1260, 1344, 1344, 1344 ]

Note the difference between `Order`

(which means the element order),
`Size`

(which means the size of the conjugacy class) and `Length`

(which
means the length of a list).

Having the conjugacy classes, we can consider class functions, i.e., maps
that are defined on the group elements, and that are constant on each
conjugacy class. One nice example is the number of fixed points; here we
use that permutations act on points via `^`

.

gap> nrfixedpoints:= function( perm, support ) > return Number( [ 1 .. support ], x -> x^perm = x ); > end; function ( perm, support ) ... end

Note that we must specify the support since a permutation does not know
about the group it is an element of; e.g. the trivial permutation `()`

has as many fixed points as the support denotes.

gap> permchar1:= List( reps, x -> nrfixedpoints( x, 8 ) ); [ 8, 0, 1, 5, 4, 1, 1, 0, 2, 2, 0, 3, 0, 0 ]

This is the character of the natural permutation representation of `a8`

(More about characters can be found in chapters Character Tables ff.).
In order to get another representation of `a8`

, we consider another
action, namely that on the elements of a conjugacy class by conjugation;
note that this is denoted by `OnPoints`

, too.

gap> class := First( ccl, c -> Size(c) = 112 ); ConjugacyClass( a8, (2,5,8) ) gap> op:= Operation( a8, Elements( class ), OnPoints );;

We get a permutation representation `op`

on 112 points. It is more
useful to look for properties than at the permutations.

gap> IsPrimitive( op, [ 1 .. 112 ] ); false gap> blocks:= Blocks( op, [ 1 .. 112 ] ); [ [ 1, 2 ], [ 6, 8 ], [ 14, 19 ], [ 17, 20 ], [ 36, 40 ], [ 32, 39 ], [ 3, 5 ], [ 4, 7 ], [ 10, 15 ], [ 65, 70 ], [ 60, 69 ], [ 54, 63 ], [ 55, 68 ], [ 50, 67 ], [ 13, 16 ], [ 27, 34 ], [ 22, 29 ], [ 28, 38 ], [ 24, 37 ], [ 31, 35 ], [ 9, 12 ], [ 106, 112 ], [ 100, 111 ], [ 11, 18 ], [ 93, 104 ], [ 23, 33 ], [ 26, 30 ], [ 94, 110 ], [ 88, 109 ], [ 49, 62 ], [ 44, 61 ], [ 43, 56 ], [ 53, 58 ], [ 48, 57 ], [ 45, 66 ], [ 59, 64 ], [ 87, 103 ], [ 81, 102 ], [ 80, 96 ], [ 92, 98 ], [ 47, 52 ], [ 42, 51 ], [ 41, 46 ], [ 82, 108 ], [ 99, 105 ], [ 21, 25 ], [ 75, 101 ], [ 74, 95 ], [ 86, 97 ], [ 76, 107 ], [ 85, 91 ], [ 73, 89 ], [ 72, 83 ], [ 79, 90 ], [ 78, 84 ], [ 71, 77 ] ] gap> op2:= Operation( op, blocks, OnSets );; gap> IsPrimitive( op2, [ 1 .. 56 ] ); true

The action of `op`

on the given block system gave us a new representation
on 56 points which is primitive, i.e., the point stabilizer is a maximal
subgroup. We compute its preimage in the representation on eight points
using homomorphisms (which of course are monomorphisms).

gap> ophom := OperationHomomorphism( a8, op );; gap> Kernel(ophom); Subgroup( s8, [ ] ) gap> ophom2:= OperationHomomorphism( op, op2 );; gap> stab:= Stabilizer( op2, 1 );; gap> Size( stab ); 360 gap> composition:= ophom * ophom2;; gap> preim:= PreImage( composition, stab ); Subgroup( s8, [ (1,3,2), (2,4,3), (1,3)(7,8), (2,3)(4,5), (6,8,7) ] )

And this is the permutation character (with respect to the succession of
conjugacy classes in `ccl`

):

gap> permchar2:= List( reps, x->nrfixedpoints(x^composition,56) ); [ 56, 0, 3, 11, 12, 0, 0, 0, 2, 2, 0, 1, 1, 1 ]

The normalizer of an element in the conjugacy class `class`

is a group of
order 360, too. In fact, it is essentially the same as the maximal

- subgroup we had found before:

gap> sgp:= Normalizer( a8, > Subgroup( s8, [ Representative(class) ] ) ); Subgroup( s8, [ (2,5)(3,4), (1,3,4), (2,5,8), (1,3,7)(2,5,8), (1,4,7,3,6)(2,5,8) ] ) gap> Size( sgp ); 360 gap> IsConjugate( a8, sgp, preim ); true

The scalar product of permutation characters of two subgroups *U*, *V*,
say, equals the number of *(U,V)*--double cosets (again, see chapters
Character Tables ff. for the details). For example, the norm of the
permutation character `permchar1`

of degree eight is two since the action
of `a8`

on the cosets of a point stabilizer is at least doubly
transitive:

gap> stab:= Stabilizer( a8, 1 );; gap> double:= DoubleCosets( a8, stab, stab ); [ DoubleCoset( Subgroup( s8, [ (3,8,7), (3,4)(7,8), (3,5,4,8,7), (3,6,5)(4,8,7), (2,6,4,5)(7,8) ] ), (), Subgroup( s8, [ (3,8,7), (3,4)(7,8), (3,5,4,8,7), (3,6,5)(4,8,7), (2,6,4,5)(7,8) ] ) ), DoubleCoset( Subgroup( s8, [ (3,8,7), (3,4)(7,8), (3,5,4,8,7), (3,6,5)(4,8,7), (2,6,4,5)(7,8) ] ), (1,2)(7,8), Subgroup( s8, [ (3,8,7), (3,4)(7,8), (3,5,4,8,7), (3,6,5)(4,8,7), (2,6,4,5)(7,8) ] ) ) ] gap> Length( double ); 2

We compute the numbers of *('sgp','sgp')* and *('sgp','stab')* double
cosets.

gap> Length( DoubleCosets( a8, sgp, sgp ) ); 4 gap> Length( DoubleCosets( a8, sgp, stab ) ); 2

Thus both irreducible constituents of `permchar1`

are also constituents
of `permchar2`

, i.e., the difference of the two permutation characters is
a proper character of `a8`

of norm two.

gap> permchar2 - permchar1; [ 48, 0, 2, 6, 8, -1, -1, 0, 0, 0, 0, -2, 1, 1 ]

One of the most important tools in group theory is the **operation** or
**action** of a group on a certain set.

We say that a group *G* operates on a set *D* if we have a function that
takes each pair *(d,g)* with *d in D* and *g in G* to another element
*d^g in D*, which we call the image of *d* under *g*, such that
*d^{identity} = d* and *(d^g)^h = d^{gh}* for each *d in D* and *g,h in
G*.

This is equivalent to saying that an operation is a homomorphism of the
group *G* into the full symmetric group on *D*. We usually call *D* the
**domain** of the operation and its elements **points**.

In this section we will demonstrate how you can compute with operations of groups. For an example we will use the alternating group on 8 points.

gap> a8 := Group( (1,2,3), (2,3,4,5,6,7,8) );; gap> a8.name := "a8";;

It is important to note however, that the applicability of the functions from the operation package is not restricted to permutation groups. All the functions mentioned in this section can also be used to compute with the operation of a matrix group on the vectors, etc. We only use a permutation group here because this makes the examples more compact.

The standard operation in **GAP** is always denoted by the caret (`^`

)
operator. That means that when no other operation is specified (we will
see below how this can be done) all the functions from the operations
package will compute the image of a point `p` under an element `g` as

. Note that this can already denote different operations,
depending on the type of points and the type of elements. For example if
the group elements are permutations it can either denote the normal
operation when the points are integers or the conjugation when the points
are permutations themselves (see Operations for Permutations). For
another example if the group elements are matrices it can either denote
the multiplication from the right when the points are vectors or again
the conjugation when the points are matrices (of the same dimension)
themselves (see Operations for Matrices). Which operations are
available through the caret operator for a particular type of group
elements is described in the chapter for this type of group elements.
`p`^`g`

gap> 2 ^ (1,2,3); 3 gap> 1 ^ a8.2; 1 gap> (2,4) ^ (1,2,3); (3,4)

The most basic function of the operations package is the function
`Orbit`

, which computes the orbit of a point under the operation of the
group.

gap> Orbit( a8, 2 ); [ 2, 3, 1, 4, 5, 6, 7, 8 ]

Note that the orbit is not a set, because it is not sorted. See Orbit for the definition in which order the points appear in an orbit.

We will try to find several subgroups in `a8`

using the operations
package. One subgroup is immediately available, namely the stabilizer of
one point. The index of the stabilizer must of course be equal to the
length of the orbit, i.e., 8.

gap> u8 := Stabilizer( a8, 1 ); Subgroup( a8, [ (2,3,4,5,6,7,8), (3,8,7) ] ) gap> Index( a8, u8 ); 8

This gives us a hint how to find further subgroups. Each subgroup is the stabilizer of a point of an appropriate transitive operation (namely the operation on the cosets of that subgroup or another operation that is equivalent to this operation).

So the question is how to find other operations. The obvious thing is to
operate on pairs of points. So using the function `Tuples`

(see
Tuples) we first generate a list of all pairs.

gap> pairs := Tuples( [1..8], 2 ); [ [ 1, 1 ], [ 1, 2 ], [ 1, 3 ], [ 1, 4 ], [ 1, 5 ], [ 1, 6 ], [ 1, 7 ], [ 1, 8 ], [ 2, 1 ], [ 2, 2 ], [ 2, 3 ], [ 2, 4 ], [ 2, 5 ], [ 2, 6 ], [ 2, 7 ], [ 2, 8 ], [ 3, 1 ], [ 3, 2 ], [ 3, 3 ], [ 3, 4 ], [ 3, 5 ], [ 3, 6 ], [ 3, 7 ], [ 3, 8 ], [ 4, 1 ], [ 4, 2 ], [ 4, 3 ], [ 4, 4 ], [ 4, 5 ], [ 4, 6 ], [ 4, 7 ], [ 4, 8 ], [ 5, 1 ], [ 5, 2 ], [ 5, 3 ], [ 5, 4 ], [ 5, 5 ], [ 5, 6 ], [ 5, 7 ], [ 5, 8 ], [ 6, 1 ], [ 6, 2 ], [ 6, 3 ], [ 6, 4 ], [ 6, 5 ], [ 6, 6 ], [ 6, 7 ], [ 6, 8 ], [ 7, 1 ], [ 7, 2 ], [ 7, 3 ], [ 7, 4 ], [ 7, 5 ], [ 7, 6 ], [ 7, 7 ], [ 7, 8 ], [ 8, 1 ], [ 8, 2 ], [ 8, 3 ], [ 8, 4 ], [ 8, 5 ], [ 8, 6 ], [ 8, 7 ], [ 8, 8 ] ]

Now we would like to have `a8`

operate on this domain. But we cannot use
the default operation (denoted by the caret) because

is not defined. So we must tell the functions from the operations
package how the group elements operate on the elements of the domain. In
our example we can do this by simply passing `list` ^ `perm``OnPairs`

as optional last
argument. All functions from the operations package accept such an
optional argument that describes the operation. See Other Operations
for a list of the available nonstandard operations.

Note that those operations are in fact simply functions that take an
element of the domain and an element of the group and return the image of
the element of the domain under the group element. So to compute the
image of the pair `[1,2]`

under the permutation `(1,4,5)`

we can simply
write

gap> OnPairs( [1,2], (1,4,5) ); [ 4, 2 ]

As was mentioned above we have to make sure that the operation is transitive. So we check this.

gap> IsTransitive( a8, pairs, OnPairs ); false

The operation is not transitive, so we want to find out what the orbits
are. The function `Orbits`

does that for you. It returns a list of all
the orbits.

gap> orbs := Orbits( a8, pairs, OnPairs ); [ [ [ 1, 1 ], [ 2, 2 ], [ 3, 3 ], [ 4, 4 ], [ 5, 5 ], [ 6, 6 ], [ 7, 7 ], [ 8, 8 ] ], [ [ 1, 2 ], [ 2, 3 ], [ 1, 3 ], [ 3, 1 ], [ 3, 4 ], [ 2, 1 ], [ 1, 4 ], [ 4, 1 ], [ 4, 5 ], [ 3, 2 ], [ 2, 4 ], [ 1, 5 ], [ 4, 2 ], [ 5, 1 ], [ 5, 6 ], [ 4, 3 ], [ 3, 5 ], [ 2, 5 ], [ 1, 6 ], [ 5, 3 ], [ 5, 2 ], [ 6, 1 ], [ 6, 7 ], [ 5, 4 ], [ 4, 6 ], [ 3, 6 ], [ 2, 6 ], [ 1, 7 ], [ 6, 4 ], [ 6, 3 ], [ 6, 2 ], [ 7, 1 ], [ 7, 8 ], [ 6, 5 ], [ 5, 7 ], [ 4, 7 ], [ 3, 7 ], [ 2, 7 ], [ 1, 8 ], [ 7, 5 ], [ 7, 4 ], [ 7, 3 ], [ 7, 2 ], [ 8, 1 ], [ 8, 2 ], [ 7, 6 ], [ 6, 8 ], [ 5, 8 ], [ 4, 8 ], [ 3, 8 ], [ 2, 8 ], [ 8, 6 ], [ 8, 5 ], [ 8, 4 ], [ 8, 3 ], [ 8, 7 ] ] ]

The operation of `a8`

on the first orbit is of course equivalent to the
original operation, so we ignore it and work with the second orbit.

gap> u56 := Stabilizer( a8, [1,2], OnPairs ); Subgroup( a8, [ (3,8,7), (3,6)(4,7,5,8), (6,7,8) ] ) gap> Index( a8, u56 ); 56

So now we have found a second subgroup. To make the following
computations a little bit easier and more efficient we would now like to
work on the points `[1..56]`

instead of the list of pairs. The function
`Operation`

does what we need. It creates a new group that operates on
`[1..56]`

in the same way that `a8`

operates on the second orbit.

gap> a8_56 := Operation( a8, orbs[2], OnPairs ); Group( ( 1, 2, 4)( 3, 6,10)( 5, 7,11)( 8,13,16)(12,18,17)(14,21,20) (19,27,26)(22,31,30)(28,38,37)(32,43,42)(39,51,50)(44,45,55), ( 1, 3, 7,12,19,28,39)( 2, 5, 9,15,23,33,45)( 4, 8,14,22,32,44, 6) (10,16,24,34,46,56,51)(11,17,25,35,47,43,55)(13,20,29,40,52,38,50) (18,26,36,48,31,42,54)(21,30,41,53,27,37,49) ) gap> a8_56.name := "a8_56";;

We would now like to know if the subgroup `u56`

of index 56 that we found
is maximal or not. Again we can make use of a function from the
operations package. Namely a subgroup is maximal if the operation on the
cosets of this subgroup is primitive, i.e., if there is no partition of
the set of cosets into subsets such that the group operates setwise on
those subsets.

gap> IsPrimitive( a8_56, [1..56] ); false

Note that we must specify the domain of the operation. You might think
that in the last example `IsPrimitive`

could use `[1..56]`

as default
domain if no domain was given. But this is not so simple, for example
would the default domain of `Group( (2,3,4) )`

be `[1..4]`

or `[2..4]`

?
To avoid confusion, all operations package functions require that you
specify the domain of operation.

We see that `a8_56`

is not primitive. This means of course that the
operation of `a8`

on `orb[2]`

is not primitive, because those two
operations are equivalent. So the stabilizer `u56`

is not maximal. Let
us try to find its supergroups. We use the function `Blocks`

to find a
block system. The (optional) third argument in the following example
tells `Blocks`

that we want a block system where 1 and 10 lie in one
block. There are several other block systems, which we could compute by
specifying a different pair, it just turns out that `[1,10]`

makes the
following computation more interesting.

gap> blocks := Blocks( a8_56, [1..56], [1,10] ); [ [ 1, 10, 13, 21, 31, 43, 45 ], [ 2, 3, 16, 20, 30, 42, 55 ], [ 4, 6, 8, 14, 22, 32, 44 ], [ 5, 7, 11, 24, 29, 41, 54 ], [ 9, 12, 17, 18, 34, 40, 53 ], [ 15, 19, 25, 26, 27, 46, 52 ], [ 23, 28, 35, 36, 37, 38, 56 ], [ 33, 39, 47, 48, 49, 50, 51 ] ]

The result is a list of sets, i.e., sorted lists, such that `a8_56`

operates on those sets. Now we would like the stabilizer of this
operation on the sets. Because we wanted to operate on the sets we have
to pass `OnSets`

as third argument.

gap> u8_56 := Stabilizer( a8_56, blocks[1], OnSets ); Subgroup( a8_56, [ (15,35,48)(19,28,39)(22,32,44)(23,33,52)(25,36,49)(26,37,50) (27,38,51)(29,41,54)(30,42,55)(31,43,45)(34,40,53)(46,56,47), ( 9,25)(12,19)(14,22)(15,34)(17,26)(18,27)(20,30)(21,31)(23,48) (24,29)(28,39)(32,44)(33,56)(35,47)(36,49)(37,50)(38,51)(40,52) (41,54)(42,55)(43,45)(46,53), ( 5,17)( 7,12)( 8,14)( 9,24)(11,18) (13,21)(15,25)(16,20)(23,47)(28,39)(29,34)(32,44)(33,56)(35,49) (36,48)(37,50)(38,51)(40,54)(41,53)(42,55)(43,45)(46,52), ( 2,11)( 3, 7)( 4, 8)( 5,16)( 9,17)(10,13)(20,24)(23,47)(25,26) (28,39)(29,30)(32,44)(33,56)(35,48)(36,50)(37,49)(38,51)(40,53) (41,55)(42,54)(43,45)(46,52), ( 1,10)( 2, 6)( 3, 4)( 5, 7)( 8,16) (12,17)(14,20)(19,26)(22,30)(23,47)(28,50)(32,55)(33,56)(35,48) (36,49)(37,39)(38,51)(40,53)(41,54)(42,44)(43,45)(46,52) ] ) gap> Index( a8_56, u8_56 ); 8

Now we have a problem. We have found a new subgroup, but not as a
subgroup of `a8`

, instead it is a subgroup of `a8_56`

. We know that
`a8_56`

is isomorphic to `a8`

(in general the result of `Operation`

is
only isomorphic to a factor group of the original group, but in this case
it must be isomorphic to `a8`

, because `a8`

is simple and has only the
full group as nontrivial factor group). But we only know that an
isomorphism exists, we do not know it.

Another function comes to our rescue. `OperationHomomorphism`

returns
the homomorphism of a group onto the group that was constructed by
`Operation`

. A later section in this chapter will introduce mappings and
homomorphisms in general, but for the moment we can just regard the
result of `OperationHomomorphism`

as a black box that we can use to
transfer information from `a8`

to `a8_56`

and back.

gap> h56 := OperationHomomorphism( a8, a8_56 ); OperationHomomorphism( a8, a8_56 ) gap> u8b := PreImages( h56, u8_56 ); Subgroup( a8, [ (6,7,8), (5,6)(7,8), (4,5)(7,8), (3,4)(7,8), (1,3)(7,8) ] ) gap> Index( a8, u8b ); 8 gap> u8 = u8b; false

So we have in fact found a new subgroup. However if we look closer we
note that `u8b`

is not totally new. It fixes the point 2, thus it lies
in the stabilizer of 2, and because it has the same index as this
stabilizer it must in fact be the stabilizer. Thus `u8b`

is conjugated
to `u8`

. A nice way to check this is to check that the operation on the
8 blocks is equivalent to the original operation.

gap> IsEquivalentOperation( a8, [1..8], a8_56, blocks, OnSets ); true

Now the choice of the third argument `[1,10]`

of `Blocks`

becomes clear.
Had we not given that argument we would have obtained the block system
that has `[1,3,7,12,19,28,39]`

as first block. The preimage of the
stabilizer of this set would have been `u8`

itself, and we would not have
been able to introduce `IsEquivalentOperation`

. Of course we could also
use the general function `IsConjugate`

, but we want to demonstrate
`IsEquivalentOperation`

.

Actually there is a third block system of `a8_56`

that gives rise to a
third subgroup.

gap> blocks := Blocks( a8_56, [1..56], [1,6] ); [ [ 1, 6 ], [ 2, 10 ], [ 3, 4 ], [ 5, 16 ], [ 7, 8 ], [ 9, 24 ], [ 11, 13 ], [ 12, 14 ], [ 15, 34 ], [ 17, 20 ], [ 18, 21 ], [ 19, 22 ], [ 23, 46 ], [ 25, 29 ], [ 26, 30 ], [ 27, 31 ], [ 28, 32 ], [ 33, 56 ], [ 35, 40 ], [ 36, 41 ], [ 37, 42 ], [ 38, 43 ], [ 39, 44 ], [ 45, 51 ], [ 47, 52 ], [ 48, 53 ], [ 49, 54 ], [ 50, 55 ] ] gap> u28_56 := Stabilizer( a8_56, [1,6], OnSets ); Subgroup( a8_56, [ ( 2,38,51)( 3,28,39)( 4,32,44)( 5,41,54)(10,43,45)(16,36,49) (17,40,53)(20,35,48)(23,47,30)(26,46,52)(33,55,37)(42,56,50), ( 5,17,26,37,50)( 7,12,19,28,39)( 8,14,22,32,44)( 9,15,23,33,54) (11,18,27,38,51)(13,21,31,43,45)(16,20,30,42,55)(24,34,46,56,49) (25,35,47,41,53)(29,40,52,36,48), ( 1, 6)( 2,39,38,19,18, 7)( 3,51,28,27,12,11)( 4,45,32,31,14,13) ( 5,55,33,23,15, 9)( 8,10,44,43,22,21)(16,50,56,46,34,24) (17,54,42,47,35,25)(20,49,37,52,40,29)(26,53,41,30,48,36) ] ) gap> u28 := PreImages( h56, u28_56 ); Subgroup( a8, [ (3,7,8), (4,5,6,7,8), (1,2)(3,8,7,6,5,4) ] ) gap> Index( a8, u28 ); 28

We know that the subgroup `u28`

of index 28 is maximal, because we know
that `a8`

has no subgroups of index 2, 4, or 7. However we can also
quickly verify this by checking that `a8_56`

operates primitively on the
28 blocks.

gap> IsPrimitive( a8_56, blocks, OnSets ); true

There is a different way to obtain `u28`

. Instead of operating on the 56
pairs `[ [1,2], [1,3], ..., [8,7] ]`

we could operate on the 28 sets of
two elements from `[1..8]`

. But suppose we make a small mistake.

gap> OrbitLength( a8, [2,1], OnSets ); Error, OnSets: <tuple> must be a set

**It is your responsibility to make sure that the points that you pass to
functions from the operations package are in normal form**. That means
that they must be sets if you operate on sets with `OnSets`

, they must be
lists of length 2 if you operate on pairs with `OnPairs`

, etc. This also
applies to functions that accept a domain of operation, e.g., `Operation`

and `IsPrimitive`

. All points in such a domain must be in normal form.
**It is not guaranteed that a violation of this rule will signal an error,
you may obtain incorrect results.**

Note that `Stabilizer`

is not only applicable to groups like `a8`

but
also to their subgroups like `u56`

. So another method to find a new
subgroup is to compute the stabilizer of another point in `u56`

. Note
that `u56`

already leaves 1 and 2 fixed.

gap> u336 := Stabilizer( u56, 3 ); Subgroup( a8, [ (4,6,5), (5,6)(7,8), (6,7,8) ] ) gap> Index( a8, u336 ); 336

Other functions are also applicable to subgroups. In the following we
show that `u336`

operates regularly on the 60 triples of `[4..8]`

which
contain no element twice, which means that this operation is equivalent
to the operations of `u336`

on its 60 elements from the right. Note that
`OnTuples`

is a generalization of `OnPairs`

.

gap> IsRegular( u336, Orbit( u336, [4,5,6], OnTuples ), OnTuples ); true

Just as we did in the case of the operation on the pairs above, we now
construct a new permutation group that operates on `[1..336]`

in the same
way that `a8`

operates on the cosets of `u336`

. Note that the operation
of a group on the cosets is by multiplication from the right, thus we
have to specify `OnRight`

.

gap> a8_336 := Operation( a8, Cosets( a8, u336 ), OnRight );; gap> a8_336.name := "a8_336";;

To find subgroups above `u336`

we again check if the operation is
primitive.

gap> blocks := Blocks( a8_336, [1..336], [1,43] ); [ [ 1, 43, 85 ], [ 2, 102, 205 ], [ 3, 95, 165 ], [ 4, 106, 251 ], [ 5, 117, 334 ], [ 6, 110, 294 ], [ 7, 122, 127 ], [ 8, 144, 247 ], [ 9, 137, 207 ], [ 10, 148, 293 ], [ 11, 45, 159 ], [ 12, 152, 336 ], [ 13, 164, 169 ], [ 14, 186, 289 ], [ 15, 179, 249 ], [ 16, 190, 335 ], [ 17, 124, 201 ], [ 18, 44, 194 ], [ 19, 206, 211 ], [ 20, 228, 331 ], [ 21, 221, 291 ], [ 22, 46, 232 ], [ 23, 166, 243 ], [ 24, 126, 236 ], [ 25, 248, 253 ], [ 26, 48, 270 ], [ 27, 263, 333 ], [ 28, 125, 274 ], [ 29, 208, 285 ], [ 30, 168, 278 ], [ 31, 290, 295 ], [ 32, 121, 312 ], [ 33, 47, 305 ], [ 34, 167, 316 ], [ 35, 250, 327 ], [ 36, 210, 320 ], [ 37, 74, 332 ], [ 38, 49, 163 ], [ 39, 81, 123 ], [ 40, 59, 209 ], [ 41, 70, 292 ], [ 42, 66, 252 ], [ 50, 142, 230 ], [ 51, 138, 196 ], [ 52, 146, 266 ], [ 53, 87, 131 ], [ 54, 153, 302 ], [ 55, 160, 174 ], [ 56, 182, 268 ], [ 57, 178, 234 ], [ 58, 189, 304 ], [ 60, 86, 199 ], [ 61, 198, 214 ], [ 62, 225, 306 ], [ 63, 218, 269 ], [ 64, 88, 235 ], [ 65, 162, 245 ], [ 67, 233, 254 ], [ 68, 90, 271 ], [ 69, 261, 301 ], [ 71, 197, 288 ], [ 72, 161, 281 ], [ 73, 265, 297 ], [ 75, 89, 307 ], [ 76, 157, 317 ], [ 77, 229, 328 ], [ 78, 193, 324 ], [ 79, 116, 303 ], [ 80, 91, 158 ], [ 82, 101, 195 ], [ 83, 112, 267 ], [ 84, 108, 231 ], [ 92, 143, 237 ], [ 93, 133, 200 ], [ 94, 150, 273 ], [ 96, 154, 309 ], [ 97, 129, 173 ], [ 98, 184, 272 ], [ 99, 180, 238 ], [ 100, 188, 308 ], [ 103, 202, 216 ], [ 104, 224, 310 ], [ 105, 220, 276 ], [ 107, 128, 241 ], [ 109, 240, 256 ], [ 111, 260, 311 ], [ 113, 204, 287 ], [ 114, 130, 277 ], [ 115, 275, 296 ], [ 118, 132, 313 ], [ 119, 239, 330 ], [ 120, 203, 323 ], [ 134, 185, 279 ], [ 135, 175, 242 ], [ 136, 192, 315 ], [ 139, 171, 215 ], [ 140, 226, 314 ], [ 141, 222, 280 ], [ 145, 244, 258 ], [ 147, 262, 318 ], [ 149, 170, 283 ], [ 151, 282, 298 ], [ 155, 246, 329 ], [ 156, 172, 319 ], [ 176, 227, 321 ], [ 177, 217, 284 ], [ 181, 213, 257 ], [ 183, 264, 322 ], [ 187, 286, 300 ], [ 191, 212, 325 ], [ 219, 259, 326 ], [ 223, 255, 299 ] ]

To find the subgroup of index 112 that belongs to this operation we could
use the same methods as before, but we actually use a different trick.
From the above we see that the subgroup is the union of `u336`

with its
43rd and its 85th coset. Thus we simply add a representative of the 43rd
coset to the generators of `u336`

.

gap> u112 := Closure( u336, Representative( Cosets(a8,u336)[43] ) ); Subgroup( a8, [ (4,6,5), (5,6)(7,8), (6,7,8), (1,3,2) ] ) gap> Index( a8, u112 ); 112

Above this subgroup of index 112 lies a subgroup of index 56, which is
not conjugate to `u56`

. In fact, unlike `u56`

it is maximal. We obtain
this subgroup in the same way that we obtained `u112`

, this time forcing
two points, namely 39 and 43 into the first block.

gap> blocks := Blocks( a8_336, [1..336], [1,39,43] );; gap> Length( blocks ); 56 gap> u56b := Closure( u112, Representative( Cosets(a8,u336)[39] ) ); Subgroup( a8, [ (4,6,5), (5,6)(7,8), (6,7,8), (1,3,2), (2,3)(7,8) ] ) gap> Index( a8, u56b ); 56 gap> IsPrimitive( a8_336, blocks, OnSets ); true

We already mentioned in the beginning that there is another standard
operation of permutations, namely the conjugation. E.g., because no
other operation is specified in the following example `OrbitLength`

simply operates using the caret operator and because

is defined as the conjugation of `perm1`^`perm2``perm2` on `perm1` we effectively
compute the length of the conjugacy class of `(1,2)(3,4)(5,6)(7,8)`

. (In
fact

is always defined as the conjugation if
`element1`^`element2``element1` and `element2` are group elements of the same type. So the
length of a conjugacy class of any element `elm` in an arbitrary group
`G` can be computed as `OrbitLength( `

. In general however
this may not be a good idea, `G`, `elm` )`Size( ConjugacyClass( `

is
probably more efficient.)
`G`, `elm` ) )

gap> OrbitLength( a8, (1,2)(3,4)(5,6)(7,8) ); 105 gap> orb := Orbit( a8, (1,2)(3,4)(5,6)(7,8) );; gap> u105 := Stabilizer( a8, (1,2)(3,4)(5,6)(7,8) ); Subgroup( a8, [ (5,6)(7,8), (1,2)(3,4)(5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (1,3)(2,4) ] ) gap> Index( a8, u105 ); 105

Of course the last stabilizer is in fact the centralizer of the element
`(1,2)(3,4)(5,6)(7,8)`

. `Stabilizer`

notices that and computes the
stabilizer using the centralizer algorithm for permutation groups.

In the usual way we now look for the subgroups that lie above `u105`

.

gap> blocks := Blocks( a8, orb );; gap> Length( blocks ); 15 gap> blocks[1]; [ (1,2)(3,4)(5,6)(7,8), (1,3)(2,4)(5,7)(6,8), (1,4)(2,3)(5,8)(6,7), (1,5)(2,6)(3,7)(4,8), (1,6)(2,5)(3,8)(4,7), (1,7)(2,8)(3,5)(4,6), (1,8)(2,7)(3,6)(4,5) ]

To find the subgroup of index 15 we again use closure. Now we must be a
little bit careful to avoid confusion. `u105`

is the stabilizer of
`(1,2)(3,4)(5,6)(7,8)`

. We know that there is a correspondence between
the points of the orbit and the cosets of `u105`

. The point
`(1,2)(3,4)(5,6)(7,8)`

corresponds to `u105`

. To get the subgroup of
index 15 we must add to `u105`

an element of the coset that corresponds
to the point `(1,3)(2,4)(5,7)(6,8)`

(or any other point in the first
block). That means that we must use an element of `a8`

that maps
`(1,2)(3,4)(5,6)(7,8)`

to `(1,3)(2,4)(5,7)(6,8)`

. The important thing is
that `(1,3)(2,4)(5,7)(6,8)`

will not do, in fact `(1,3)(2,4)(5,7)(6,8)`

lies in `u105`

.

The function `RepresentativeOperation`

does what we need. It takes a
group and two points and returns an element of the group that maps the
first point to the second. In fact it also allows you to specify the
operation as optional fourth argument as usual, but we do not need this
here. If no such element exists in the group, i.e., if the two points do
not lie in one orbit under the group, `RepresentativeOperation`

returns
`false`

.

gap> rep := RepresentativeOperation( a8, (1,2)(3,4)(5,6)(7,8), > (1,3)(2,4)(5,7)(6,8) ); (2,3)(6,7) gap> u15 := Closure( u105, rep ); Subgroup( a8, [ (5,6)(7,8), (1,2)(3,4)(5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (1,3)(2,4), (2,3)(6,7) ] ) gap> Index( a8, u15 ); 15

`u15`

is of course a maximal subgroup, because `a8`

has no subgroups of
index 3 or 5.

There is in fact another class of subgroups of index 15 above `u105`

that
we get by adding `(2,3)(6,8)`

to `u105`

.

gap> u15b := Closure( u105, (2,3)(6,8) ); Subgroup( a8, [ (5,6)(7,8), (1,2)(3,4)(5,6)(7,8), (5,7)(6,8), (3,4)(7,8), (3,5)(4,6), (1,3)(2,4), (2,3)(6,8) ] ) gap> Index( a8, u15b ); 15

We now show that `u15`

and `u15b`

are not conjugate. We showed that `u8`

and `u8b`

are conjugate by showing that the operations on the cosets
where equivalent. We could show that `u15`

and `u15b`

are not conjugate
by showing that the operations on their cosets are not equivalent.
Instead we simply call `RepresentativeOperation`

again.

gap> RepresentativeOperation( a8, u15, u15b ); false

`RepresentativeOperation`

tells us that there is no element `g` in `a8`

such that `u15^`

. Because `g` = u15b`^`

also denotes the conjugation
of subgroups this tells us that `u15`

and `u15b`

are not conjugate. Note
that this operation should only be used rarely, because it is usually not
very efficient. The test in this case is however reasonable efficient,
and is in fact the one employed by `IsConjugate`

(see IsConjugate).

This concludes our example. In this section we demonstrated some
functions from the operations package. There is a whole class of
functions that we did not mention, namely those that take a single
element instead of a whole group as first argument, e.g., `Cycle`

and
Operations of Groups.

In this section we will show you the investigation of a Coxeter group
that is given by its presentation. You will see that finitely presented
groups and presentations are different kinds of objects in **GAP**. While
finitely presented groups can never be changed after they have been
created as factor groups of free groups, presentations allow
manipulations of the generators and relators by Tietze transformations.
The investigation of the example will involve methods and algorithms like
Todd-Coxeter, Reidemeister-Schreier, Nilpotent Quotient, and Tietze
transformations.

We start by defining a Coxeter group `c`

on five generators as a factor
group of the free group of rank 5, whose generators we already call
`c.1`

, ..., `c.5`

.

gap> c := FreeGroup( 5, "c" );; gap> r := List( c.generators, x -> x^2 );; gap> Append( r, [ (c.1*c.2)^3, (c.1*c.3)^2, (c.1*c.4)^3, > (c.1*c.5)^3, (c.2*c.3)^3, (c.2*c.4)^2, (c.2*c.5)^3, > (c.3*c.4)^3, (c.3*c.5)^3, (c.4*c.5)^3, > (c.1*c.2*c.5*c.2)^2, (c.3*c.4*c.5*c.4)^2 ] ); gap> c := c / r; Group( c.1, c.2, c.3, c.4, c.5 )

If we call the function `Size`

for this group **GAP** will invoke the
Todd-Coxeter method, which however will fail to get a result going up to
the default limit of defining 64000 cosets:

gap> Size(c); Error, the coset enumeration has defined more than 64000 cosets: type 'return;' if you want to continue with a new limit of 128000 cosets, type 'quit;' if you want to quit the coset enumeration, type 'maxlimit := 0; return;' in order to continue without a limit, in AugmentedCosetTableMtc( G, H, -1, "_x" ) called from D.operations.Size( D ) called from Size( c ) called from main loop brk> quit;

In fact, as we shall see later, our finitely presented group is infinite and hence we would get the same answer also with larger limits. So we next look for subgroups of small index, in our case limiting the index to four.

gap> lis := LowIndexSubgroupsFpGroup( c, TrivialSubgroup(c), 4 );; gap> Length(lis); 10

The `LowIndexSubgroupsFpGroup`

function in fact determines generators for
the subgroups, written in terms of the generators of the given group. We
can find the index of these subgroups by the function `Index`

, and the
permutation representation on the cosets of these subgroups by the
function `OperationCosetsFpGroup`

, which use a Todd-Coxeter method. The
size of the image of this permutation representation is found using
`Size`

which in this case uses a Schreier-Sims method for permutation
groups.

gap> List(lis, x -> [Index(c,x),Size(OperationCosetsFpGroup(c,x))]); [ [ 1, 1 ], [ 4, 24 ], [ 4, 24 ], [ 4, 24 ], [ 4, 24 ], [ 4, 24 ], [ 4, 24 ], [ 4, 24 ], [ 3, 6 ], [ 2, 2 ] ]

We next determine the commutator factor groups of the kernels of these
permutation representations. Note that here the difference of finitely
presented groups and presentations has to be observed: We first
determine the kernel of the permutation representation by the function
`Core`

as a subgroup of `c`

, then a presentation of this subgroup using
`PresentationSubgroup`

, which has to be converted into a finitely
presented group of its own right using `FpGroupPresentation`

, before its
commutator factor group and the abelian invariants can be found using
integer matrix diagonalisation of the relators matrix by an elementary
divisor algorithm. The conversion is necessary because `Core`

computes a
subgroup given by words in the generators of `c`

but
`CommutatorFactorGroup`

needs a parent group given by generators and
relators.

gap> List( lis, x -> AbelianInvariants( CommutatorFactorGroup( > FpGroupPresentation( PresentationSubgroup( c, Core(c,x) ) ) ) ) ); [ [ 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 0, 0, 0, 0, 0, 0 ], [ 2, 2, 2, 2, 2, 2 ], [ 3 ] ]

More clearly arranged, this is

[ [ 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 2, 2, 2, 2, 2, 2, 2, 2 ], [ 0, 0, 0, 0, 0, 0 ], [ 2, 2, 2, 2, 2, 2 ], [ 3 ] ]

Note that there is another function `AbelianInvariantsSubgroupFpGroup`

which we could have used to obtain this list which will do an abelianized
Reduced Reidemeister-Schreier. This function is much faster because it
does not compute a complete presentation for the core.

The output obtained shows that the third last of the kernels has a free
abelian commutator factor group of rank 6. We turn our attention to this
kernel which we call `n`

, while we call the associated presentation `pr`

.

gap> lis[8]; Subgroup( Group( c.1, c.2, c.3, c.4, c.5 ), [ c.1, c.2, c.3*c.2*c.5^-1, c.3*c.4*c.3^-1, c.4*c.1*c.5^-1 ] ) gap> pr := PresentationSubgroup( c, Core( c, lis[8] ) ); << presentation with 22 gens and 41 rels of total length 156 >> gap> n := FpGroupPresentation(pr);;

We first determine *p*-factor groups for primes *2*, *3*, *5*, and *7*.

gap> InfoPQ1:= Ignore;; gap> List( [2,3,5,7], p -> PrimeQuotient(n,p,5).dimensions ); [ [ 6, 10, 18, 30, 54 ], [ 6, 10, 18, 30, 54 ], [ 6, 10, 18, 30, 54 ], [ 6, 10, 18, 30, 54 ] ]

Observing that the ranks of the lower exponent-*p* central series are the
same for these primes we suspect that the lower central series may have
free abelian factors. To investigate this we have to call the package
"nq".

gap> RequirePackage("nq"); gap> NilpotentQuotient( n, 5 ); [ [ 0, 0, 0, 0, 0, 0 ], [ 0, 0, 0, 0 ], [ 0, 0, 0, 0, 0, 0, 0, 0 ], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ] gap> List( last, Length ); [ 6, 4, 8, 12, 24 ]

The ranks of the factors except the first are divisible by four, and we compare them with the corresponding ranks of a free group on two generators.

gap> f2 := FreeGroup(2); Group( f.1, f.2 ) gap> PrimeQuotient( f2, 2, 5 ).dimensions; [ 2, 3, 5, 8, 14 ] gap> NilpotentQuotient( f2, 5 ); [ [ 0, 0 ], [ 0 ], [ 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0, 0, 0, 0 ] ] gap> List( last, Length ); [ 2, 1, 2, 3, 6 ]

The result suggests a close relation of our group to the direct product
of four free groups of rank two. In order to study this we want a simple
presentation for our kernel `n`

and obtain this by repeated use of Tietze
transformations, using first the default simplification function `TzGoGo`

and later specific introduction of new generators that are obtained as
product of two of the existing ones using the function `TzSubstitute`

.
(Of course, this latter sequence of Tietze transformations that we
display here has only been found after some trial and error.)

gap> pr := PresentationSubgroup( c, Core( c, lis[8] ) ); << presentation with 22 gens and 41 rels of total length 156 >> gap> TzGoGo(pr); #I there are 6 generators and 14 relators of total length 74 gap> TzGoGo(pr); #I there are 6 generators and 13 relators of total length 66 gap> TzGoGo(pr); gap> TzPrintPairs(pr); #I 1. 3 occurrences of _x6 * _x11^-1 #I 2. 3 occurrences of _x3 * _x15 #I 3. 2 occurrences of _x11^-1 * _x15^-1 #I 4. 2 occurrences of _x6 * _x15 #I 5. 2 occurrences of _x6^-1 * _x15^-1 #I 6. 2 occurrences of _x4 * _x15 #I 7. 2 occurrences of _x4^-1 * _x15^-1 #I 8. 2 occurrences of _x4^-1 * _x11 #I 9. 2 occurrences of _x4 * _x6 #I 10. 2 occurrences of _x3^-1 * _x11 gap> TzSubstitute(pr,10,2); #I substituting new generator _x26 defined by _x3^-1*_x11 #I eliminating _x11 = _x3*_x26 #I there are 6 generators and 13 relators of total length 70 gap> TzGoGo(pr); #I there are 6 generators and 12 relators of total length 62 #I there are 6 generators and 12 relators of total length 60 gap> TzGoGo(pr); gap> TzSubstitute(pr,9,2); #I substituting new generator _x27 defined by _x1^-1*_x15 #I eliminating _x15 = _x27*_x1 #I there are 6 generators and 12 relators of total length 64 gap> TzGoGo(pr); #I there are 6 generators and 11 relators of total length 56 gap> TzGoGo(pr); gap> p2 := Copy(pr); << presentation with 6 gens and 11 rels of total length 56 >> gap> TzPrint(p2); #I generators: [ _x1, _x3, _x4, _x6, _x26, _x27 ] #I relators: #I 1. 4 [ -6, -1, 6, 1 ] #I 2. 4 [ 4, 6, -4, -6 ] #I 3. 4 [ 5, 4, -5, -4 ] #I 4. 4 [ 4, -2, -4, 2 ] #I 5. 4 [ -3, 2, 3, -2 ] #I 6. 4 [ -3, -1, 3, 1 ] #I 7. 6 [ -4, 3, 4, 6, -3, -6 ] #I 8. 6 [ -1, -6, -2, 6, 1, 2 ] #I 9. 6 [ -6, -2, -5, 6, 2, 5 ] #I 10. 6 [ 2, 5, 1, -5, -2, -1 ] #I 11. 8 [ -1, -6, -5, 3, 6, 1, 5, -3 ] gap> TzPrintPairs(p2); #I 1. 5 occurrences of _x1^-1 * _x27^-1 #I 2. 3 occurrences of _x6 * _x27 #I 3. 3 occurrences of _x3 * _x26 #I 4. 2 occurrences of _x3 * _x27 #I 5. 2 occurrences of _x1 * _x4 #I 6. 2 occurrences of _x1 * _x3 #I 7. 1 occurrence of _x26 * _x27 #I 8. 1 occurrence of _x26 * _x27^-1 #I 9. 1 occurrence of _x26^-1 * _x27 #I 10. 1 occurrence of _x6 * _x27^-1 gap> TzSubstitute(p2,1,2); #I substituting new generator _x28 defined by _x1^-1*_x27^-1 #I eliminating _x27 = _x1^-1*_x28^-1 #I there are 6 generators and 11 relators of total length 58 gap> TzGoGo(p2); #I there are 6 generators and 11 relators of total length 54 gap> TzGoGo(p2); gap> p3 := Copy(p2); << presentation with 6 gens and 11 rels of total length 54 >> gap> TzSubstitute(p3,3,2); #I substituting new generator _x29 defined by _x3*_x26 #I eliminating _x26 = _x3^-1*_x29 gap> TzGoGo(p3); #I there are 6 generators and 11 relators of total length 52 gap> TzGoGo(p3); gap> TzPrint(p3); #I generators: [ _x1, _x3, _x4, _x6, _x28, _x29 ] #I relators: #I 1. 4 [ 6, 4, -6, -4 ] #I 2. 4 [ 1, -6, -1, 6 ] #I 3. 4 [ -5, -1, 5, 1 ] #I 4. 4 [ -2, -5, 2, 5 ] #I 5. 4 [ 4, -2, -4, 2 ] #I 6. 4 [ -3, 2, 3, -2 ] #I 7. 4 [ -3, -1, 3, 1 ] #I 8. 6 [ -2, 5, -6, 2, -5, 6 ] #I 9. 6 [ 4, -1, -5, -4, 5, 1 ] #I 10. 6 [ -6, 3, -5, 6, -3, 5 ] #I 11. 6 [ 3, -5, 4, -3, -4, 5 ]

The resulting presentation could further be simplified by Tietze
transformations using `TzSubstitute`

and `TzGoGo`

until one reaches
finally a presentation on 6 generators with 11 relators, 9 of which are
commutators of the generators. Working by hand from these, the kernel
can be identified as a particular subgroup of the direct product of four
copies of the free group on two generators.

In this section we will show you some basic computations with fields.
**GAP** supports at present the following fields. The rationals,
cyclotomic extensions of rationals and their subfields (which we will
refer to as number fields in the following), and finite fields.

Let us first take a look at the infinite fields mentioned above. While
the set of rational numbers is a predefined domain in **GAP** to which you
may refer by its identifier `Rationals`

, cyclotomic fields are
constructed by using the function `CyclotomicField`

, which may be
abbreviated as `CF`

.

gap> IsField( Rationals ); true gap> Size( Rationals ); "infinity" gap> f := CyclotomicField( 8 ); CF(8) gap> IsSubset( f, Rationals ); true

The integer argument `n`

of the function call to `CF`

specifies that the
cyclotomic field containing all n-th roots of unity should be returned.

Cyclotomic fields are constructed as extensions of the Rationals by
primitive roots of unity. Thus a primitive n-th root of unity is always
an element of CF(n), where n is a natural number. In **GAP**, one may
construct a primitive n-th root of unity by calling `E(n)`

.

gap> (E(8) + E(8)^3)^2; -2 gap> E(8) in f; true

For every field extension you can compute the Galois group, i.e., the group of automorphisms that leave the subfield fixed. For an example, cyclotomic fields are an extension of the rationals, so you can compute their Galois group over the rationals.

gap> Galf := GaloisGroup( f ); Group( NFAutomorphism( CF(8) , 7 ), NFAutomorphism( CF(8) , 5 ) ) gap> Size( Galf ); 4

The above cyclotomic field is a small example where the Galois group is not cyclic.

gap> IsCyclic( Galf ); false gap> IsAbelian( Galf ); true gap> AbelianInvariants( Galf ); [ 2, 2 ]

This shows us that the 8th cyclotomic field has a Galois group which is
isomorphic to group *V_4*.

The elements of the Galois group are **GAP** automorphisms, so they may be
applied to the elements of the field in the same way as all mappings are
usually applied to objects in **GAP**.

gap> g := Galf.generators[1]; NFAutomorphism( CF(8) , 7 ) gap> E(8) ^ g; -E(8)^3

There are two functions, `Norm`

and `Trace`

, which compute the norm and
the trace of elements of the field, respectively. The norm and the trace
of an element *a* are defined to be the product and the sum of the images
of *a* under the Galois group. You should usually specify the field as a
first argument. This argument is however optional. If you omit a
default field will be used. For a cyclotomic *a* this is the smallest
cyclotomic field that contains *a* (note that this is not the smallest
field that contains *a*, which may be a number field that is not a
cyclotomic field).

gap> orb := List( Elements( Galf ), x -> E(8) ^ x ); [ E(8), E(8)^3, -E(8), -E(8)^3 ] gap> Sum( orb ) = Trace( f, E(8) ); true gap> Product( orb ) = Norm( f, E(8) ); true gap> Trace( f, 1 ); 4

The basic way to construct a finite field is to use the function
`GaloisField`

which may be abbreviated, as usual in algebra, as `GF`

.
Thus

gap> k := GF( 3, 4 ); GF(3^4)

or

gap> k := GaloisField( 81 ); GF(3^4)

will assign the finite field of order *3^4* to the variable `k`

.

In fact, what `GF`

does is to set up a record which contains all
necessary information, telling that it represents a finite field of
degree 4 over its prime field with 3 elements. Of course, all arguments
to `GF`

others than those which represent a prime power are rejected --
for obvious reasons.

Some of the more important entries of the field record are `zero`

, `one`

and `root`

, which hold the corresponding elements of the field. All
elements of a finite field are represented as a certain power of an
appropriate primitive root, which is written as `Z(`

. As can be seen
below the smallest possible primitive root is used.
`q`)

gap> k.one + k.root + k.root^10 - k.zero; Z(3^4)^52 gap> k.root; Z(3^4) gap> k.root ^ 20; Z(3^2)^2 gap> k.one; Z(3)^0

Note that of course elements from fields of different characteristic cannot be combined in operations.

gap> Z(3^2) * k.root + k.zero + Z(3^8); Z(3^8)^6534 gap> Z(2) * k.one; Error, Finite field *: operands must have the same characteristic

In this example we tried to multiply a primitive root of the field with
two elements with the identity element of the field `k`

. As the
characteristic of `k`

equals 3, there is no way to perform the
multiplication. The first statement of the example shows, that if all
the elements of the expression belong to fields of the same
characteristic, the result will be computed.

As soon as a primitive root is demanded, **GAP** internally sets up all
relevant data structures that are necessary to compute in the
corresponding finite field. Each finite field is constructed as a
splitting field of a Conway polynomial. These polynomials, as a set,
have special properties that make it easy to embed smaller fields in
larger ones and to convert the representation of the elements when doing
so. All Conway polynomials for fields up to an order of 65536 have been
computed and installed in the **GAP** kernel.

But now look at the following example.

gap> Z(3^3) * Z(3^4); Error, Finite field *: smallest common superfield to large

Although both factors are elements of fields of characteristic 3, the
product can not be evaluated by **GAP**. The reason for this is very easy
to explain: In order to compute the product, **GAP** has to find a field
in which both of the factors lie. Here in our example the smallest field
containing *Z(3^3)* and *Z(3^4)* is *GF(3^{12})*, the field with *531441*
elements. As we have mentioned above that the size of finite fields in
**GAP** is limited at present by *65536* we now see that there is no
chance to set up the internal data structures for the common field to
perform the computation.

As before with cyclotomic fields, the Galois group of a finite field and the norm and trace of its elements may be computed. The calling conventions are the same as for cyclotomic fields.

gap> Galk := GaloisGroup( k ); Group( FrobeniusAutomorphism( GF(3^4) ) ) gap> Size( Galk ); 4 gap> IsCyclic( Galk ); true gap> Norm( k, k.root ^ 20 ); Z(3)^0 gap> Trace( k, k.root ^ 20 ); 0*Z(3)

So far, in our examples, we were always interested in the Galois group of
a field extension *k* over its prime field. In fact it often will occur
that, given a subfield *l* of *k* the Galois group of *k* over *l* is
desired. In **GAP** it is possible to change the structure of a field by
using the `/`

operator. So typing

gap> l := GF(3^2); GF(3^2) gap> IsSubset( k, l ); true gap> k / l; GF(3^4)/GF(3^2)

changes the representation of *k* from a field extension of degree 4 over
*GF(3)* to a field given as an extension of degree 2 over *GF(3^2)*. The
actual elements of the fields are still the same, only the structure of
the field has changed.

gap> k = k / l; true gap> Galkl := GaloisGroup( k / l ); Group( FrobeniusAutomorphism( GF(3^4)/GF(3^2) )^2 ) gap> Size( Galkl ); 2

Of course, all the relevant functions behave in a different way when they
refer to `k / l`

instead of `k`

gap> Norm( k / l, k.root ^ 20 ); Z(3) gap> Trace( k / l, k.root ^ 20 ); Z(3^2)^6

This feature, to change the structure of the field without changing the underlying set of elements, is also available for cyclotomic fields, which we have seen at the beginning of this chapter.

gap> g := CyclotomicField( 4 ); GaussianRationals gap> IsSubset( f, g ); true gap> f / g; CF(8)/GaussianRationals gap> Galfg := GaloisGroup( f / g ); Group( NFAutomorphism( CF(8)/GaussianRationals , 5 ) ) gap> Size( Galfg ); 2

The examples should have shown that, although the structure of finite
fields and cyclotomic fields is rather different, there is a similar
interface to them in **GAP**, which makes it easy to write programs that
deal with both types of fields in the same way.

This section intends to show you the things you could do with matrix
groups in **GAP**. In principle all the set theoretic functions mentioned
in chapter Domains and all group functions mentioned in chapter
Groups can be applied to matrix groups. However, you should note that
at present only very few functions can work efficiently with matrix
groups. Especially infinite matrix groups (over the rationals or
cyclotomic fields) can not be dealt with at all.

Matrix groups are created in the same way as the other types of groups,
by using the function `Group`

. Of course, in this case the arguments
have to be invertable matrices over a field.

gap> m1 := [ [ Z(3)^0, Z(3)^0, Z(3) ], > [ Z(3), 0*Z(3), Z(3) ], > [ 0*Z(3), Z(3), 0*Z(3) ] ];; gap> m2 := [ [ Z(3), Z(3), Z(3)^0 ], > [ Z(3), 0*Z(3), Z(3) ], > [ Z(3)^0, 0*Z(3), Z(3) ] ];; gap> m := Group( m1, m2 ); Group( [ [ Z(3)^0, Z(3)^0, Z(3) ], [ Z(3), 0*Z(3), Z(3) ], [ 0*Z(3), Z(3), 0*Z(3) ] ], [ [ Z(3), Z(3), Z(3)^0 ], [ Z(3), 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3), Z(3) ] ] )

As usual for groups, the matrix group that we have constructed is represented by a record with several entries. For matrix groups, there is one additional entry which holds the field over which the matrix group is written.

gap> m.field; GF(3)

Note that you do not specify the field when you construct the group.
`Group`

automatically takes the smallest field over which all its
arguments can be written.

At this point there is the question what special functions are available
for matrix groups. The size of our group, for example, may be computed
using the function `Size`

.

gap> Size( m ); 864

If we now compute the size of the corresponding general linear group

gap> (3^3 - 3^0) * (3^3 - 3^1) * (3^3 - 3^2); 11232

we see that we have constructed a proper subgroup of index 13 of
*GL(3,3)*.

Let us now set up a subgroup of `m`

, which is generated by the matrix
`m2`

.

gap> n := Subgroup( m, [ m2 ] ); Subgroup( Group( [ [ Z(3)^0, Z(3)^0, Z(3) ], [ Z(3), 0*Z(3), Z(3) ], [ 0*Z(3), Z(3), 0*Z(3) ] ], [ [ Z(3), Z(3), Z(3)^0 ], [ Z(3), 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3), Z(3) ] ] ), [ [ [ Z(3), Z(3), Z(3)^0 ], [ Z(3), 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3), Z(3) ] ] ] ) gap> Size( n ); 6

And to round up this example we now compute the centralizer of this
subgroup in `m`

.

gap> c := Centralizer( m, n ); Subgroup( Group( [ [ Z(3)^0, Z(3)^0, Z(3) ], [ Z(3), 0*Z(3), Z(3) ], [ 0*Z(3), Z(3), 0*Z(3) ] ], [ [ Z(3), Z(3), Z(3)^0 ], [ Z(3), 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3), Z(3) ] ] ), [ [ [ Z(3), Z(3), Z(3)^0 ], [ Z(3), 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3), Z(3) ] ], [ [ Z(3), 0*Z(3), 0*Z(3) ], [ 0*Z(3), Z(3), 0*Z(3) ], [ 0*Z(3), 0*Z(3), Z(3) ] ] ] ) gap> Size( c ); 12

In this section you have seen that matrix groups are constructed in the same way that all groups are constructed. You have also been warned that only very few functions can work efficiently with matrix groups. See chapter Matrix Groups to read more about matrix groups.

**Domain** is **GAP**'s name for structured sets. We already saw examples
of domains in the previous sections. For example, the groups `s8`

and
`a8`

in sections About Groups and About Operations of Groups are
domains. Likewise the fields in section About Fields are domains.
**Categories** are sets of domains. For example, the set of all groups
forms a category, as does the set of all fields.

In those sections we treated the domains as black boxes. They were
constructed by special functions such as `Group`

and `GaloisField`

, and
they could be passed as arguments to other functions such as `Size`

and
`Orbits`

.

In this section we will also treat domains as black boxes. We will
describe how domains are created in general and what functions are
applicable to all domains. Next we will show how domains with the same
structure are grouped into categories and will give an overview of the
categories that are available. Then we will discuss how the organization
of the **GAP** library around the concept of domains and categories is
reflected in this manual. In a later section we will open the black
boxes and give an overview of the mechanism that makes all this work (see
About the Implementation of Domains).

The first thing you must know is how you can obtain domains. You have basically three possibilities. You can use the domains that are predefined in the library, you can create new domains with domain constructors, and you can use the domains returned by many library functions. We will now discuss those three possibilities in turn.

The **GAP** library predefines some domains. That means that there is a
global variable whose value is this domain. The following example shows
some of the more important predefined domains.

gap> Integers; Integers # the ring of all integers gap> Size( Integers ); "infinity" gap> GaussianRationals; GaussianRationals # the field of all Gaussian gap> (1/2+E(4)) in GaussianRationals; true # 'E(4)' is {\GAP}\'s name for the complex root of -1 gap> Permutations; Permutations # the domain of all permutations

Note that **GAP** prints those domains using the name of the global
variable.

You can create new domains using **domain constructors** such as `Group`

,
`Field`

, etc. A domain constructor is a function that takes a certain
number of arguments and returns the domain described by those arguments.
For example, `Group`

takes an arbitrary number of group elements (of the
same type) and returns the group generated by those elements.

gap> gf16 := GaloisField( 16 ); GF(2^4) # the finite field with 16 elements gap> Intersection( gf16, GaloisField( 64 ) ); GF(2^2) gap> a5 := Group( (1,2,3), (3,4,5) ); Group( (1,2,3), (3,4,5) ) # the alternating group on 5 points gap> Size( a5 ); 60

Again **GAP** prints those domains using more or less the expression that
you entered to obtain the domain.

As with groups (see About Groups) a name can be assigned to an
arbitrary domain `D` with the assignment

, and
`D`.name := `string`;**GAP** will use this name from then on in the output.

Many functions in the **GAP** library return domains. In the last example
you already saw that `Intersection`

returned a finite field domain.
Below are more examples.

gap> GaloisGroup( gf16 ); Group( FrobeniusAutomorphism( GF(2^4) ) ) gap> SylowSubgroup( a5, 2 ); Subgroup( Group( (1,2,3), (3,4,5) ), [ (2,4)(3,5), (2,3)(4,5) ] )

The distinction between domain constructors and functions that return domains is a little bit arbitrary. It is also not important for the understanding of what follows. If you are nevertheless interested, here are the principal differences. A constructor performs no computation, while a function performs a more or less complicated computation. A constructor creates the representation of the domain, while a function relies on a constructor to create the domain. A constructor knows the dirty details of the domain's representation, while a function may be independent of the domain's representation. A constructor may appear as printed representation of a domain, while a function usually does not.

After showing how domains are created, we will now discuss what you can
do with domains. You can assign a domain to a variable, put a domain
into a list or into a record, pass a domain as argument to a function,
and return a domain as result of a function. In this regard there is no
difference between an integer value such as 17 and a domain such as
`Group( (1,2,3), (3,4,5) )`

. Of course many functions will signal an
error when you call them with domains as arguments. For example, `Gcd`

does not accept two groups as arguments, because they lie in no Euclidean
ring.

There are some functions that accept domains of any type as their
arguments. Those functions are called the **set theoretic functions**.
The full list of set theoretic functions is given in chapter Domains.

Above we already used one of those functions, namely `Size`

. If you look
back you will see that we applied `Size`

to the domain `Integers`

, which
is a ring, and the domain `a5`

, which is a group. Remember that a domain
was a structured set. The size of the domain is the number of elements
in the set. `Size`

returns this number or the string `"infinity"`

if
the domain is infinite. Below are more examples.

gap> Size( GaussianRationals ); "infinity" # this string is returned for infinite domains gap> Size( SylowSubgroup( a5, 2 ) ); 4

`IsFinite( `

returns `D` )`true`

if the domain `D` is finite and `false`

otherwise. You could also test if a domain is finite using `Size( `

(`D` )
< "infinity"**GAP** evaluates

to `n` < "infinity"`true`

for
any number `n`). `IsFinite`

is more efficient. For example, if `D` is a
permutation group, `IsFinite( `

can immediately return `D` )`true`

, while
`Size( `

may take quite a while to compute the size of `D` )`D`.

The other function that you already saw is `Intersection`

. Above we
computed the intersection of the field with 16 elements and the field
with 64 elements. The following example is similar.

gap> Intersection( a5, Group( (1,2), (1,2,3,4) ) ); Group( (2,3,4), (1,2)(3,4) ) # alternating group on 4 points

In general `Intersection`

tries to return a domain. In general this is
not possible however. Remember that a domain is a structured set. If
the two domain arguments have different structure the intersection may
not have any structure at all. In this case `Intersection`

returns the
result as a proper set, i.e., as a sorted list without holes and
duplicates. The following example shows such a case. `ConjugacyClass`

returns the conjugacy class of `(1,2,3,4,5)`

in the alternating group on
6 points as a domain. If we intersect this class with the symmetric
group on 5 points we obtain a proper set of 12 permutations, which is
only one half of the conjugacy class of 5 cycles in `s5`

.

gap> a6 := Group( (1,2,3), (2,3,4,5,6) ); Group( (1,2,3), (2,3,4,5,6) ) gap> class := ConjugacyClass( a6, (1,2,3,4,5) ); ConjugacyClass( Group( (1,2,3), (2,3,4,5,6) ), (1,2,3,4,5) ) gap> Size( class ); 72 gap> s5 := Group( (1,2), (2,3,4,5) ); Group( (1,2), (2,3,4,5) ) gap> Intersection( class, s5 ); [ (1,2,3,4,5), (1,2,4,5,3), (1,2,5,3,4), (1,3,5,4,2), (1,3,2,5,4), (1,3,4,2,5), (1,4,3,5,2), (1,4,5,2,3), (1,4,2,3,5), (1,5,4,3,2), (1,5,2,4,3), (1,5,3,2,4) ]

You can intersect arbitrary domains as the following example shows.

gap> Intersection( Integers, a5 ); [ ] # the empty set

Note that we optimized `Intersection`

for typical cases, e.g., computing
the intersection of two permutation groups, etc. The above computation
is done with a very simple--minded method, all elements of `a5`

are
listed (with `Elements`

, described below), and for each element
`Intersection`

tests whether it lies in `Integers`

(with `in`

, described
below). So the same computation with the alternating group on 10 points
instead of `a5`

will probably exhaust your patience.

Just as `Intersection`

returns a proper set occasionally, it also accepts
proper sets as arguments. `Intersection`

also takes an arbitrary number
of arguments. And finally it also accepts a list of domains or sets to
intersect as single argument.

gap> Intersection( a5, [ (1,2), (1,2,3), (1,2,3,4), (1,2,3,4,5) ] ); [ (1,2,3), (1,2,3,4,5) ] gap> Intersection( [2,4,6,8,10], [3,6,9,12,15], [5,10,15,20,25] ); [ ] gap> Intersection( [ [1,2,4], [2,3,4], [1,3,4] ] ); [ 4 ]

The function `Union`

is the obvious counterpart of `Intersection`

. Note
that `Union`

usually does **not** return a domain. This is because the
union of two domains, even of the same type, is usually not again a
domain of that type. For example, the union of two subgroups is a
subgroup if and only if one of the subgroups is a subset of the other.
Of course this is exactly the reason why `Union`

is less important than
`Intersection`

in algebra.

Because domains are structured sets there ought to be a membership test
that tests whether an object lies in this domain or not. This is not
implemented by a function, instead the operator `in`

is used.

returns `elm` in
`D``true`

if the element `elm` lies in the domain `D` and
`false`

otherwise. We already used the `in`

operator above when we
tested whether `1/2 + E(4)`

lies in the domain of Gaussian integers.

gap> (1,2,3) in a5; true gap> (1,2) in a5; false gap> (1,2,3,4,5,6,7) in a5; false gap> 17 in a5; false # of course an integer does not lie in a permutation group gap> a5 in a5; false

As you can see in the last example, `in`

only implements the membership
test. It does not allow you to test whether a domain is a subset of
another domain. For such tests the function `IsSubset`

is available.

gap> IsSubset( a5, a5 ); true gap> IsSubset( a5, Group( (1,2,3) ) ); true gap> IsSubset( Group( (1,2,3) ), a5 ); false

In the above example you can see that `IsSubset`

tests whether the second
argument is a subset of the first argument. As a general rule **GAP**
library functions take as **first** arguments those arguments that are in
some sense **larger** or more structured.

Suppose that you want to loop over all elements of a domain. For
example, suppose that you want to compute the set of element orders of
elements in the group `a5`

. To use the `for`

loop you need a list of
elements in the domain `D`, because `for `

will not work. The function `var` in `D` do `statements` od`Elements`

does exactly that. It takes a
domain `D` and returns the proper set of elements of `D`.

gap> Elements( Group( (1,2,3), (2,3,4) ) ); [ (), (2,3,4), (2,4,3), (1,2)(3,4), (1,2,3), (1,2,4), (1,3,2), (1,3,4), (1,3)(2,4), (1,4,2), (1,4,3), (1,4)(2,3) ] gap> ords := [];; gap> for elm in Elements( a5 ) do > Add( ords, Order( a5, elm ) ); > od; gap> Set( ords ); [ 1, 2, 3, 5 ] gap> Set( List( Elements( a5 ), elm -> Order( a5, elm ) ) ); [ 1, 2, 3, 5 ] # an easier way to compute the set of orders

Of course, if you apply `Elements`

to an infinite domain, `Elements`

will
signal an error. It is also not a good idea to apply `Elements`

to very
large domains because the list of elements will take much space and
computing this large list will probably exhaust your patience.

gap> Elements( GaussianIntegers ); Error, the ring <R> must be finite to compute its elements in D.operations.Elements( D ) called from Elements( GaussianIntegers ) called from main loop brk> quit;

There are a few more set theoretic functions. See chapter Domains for a complete list.

All the set theoretic functions treat the domains as if they had no structure. Now a domain is a structured set (excuse us for repeating this again and again, but it is really important to get this across). If the functions ignore the structure than they are effectively viewing a domain only as the set of elements.

In fact all set theoretic functions also accept proper sets, i.e., sorted
lists without holes and duplicates as arguments (we already mentioned
this for `Intersection`

). Also set theoretic functions may occasionally
return proper sets instead of domains as result.

This equivalence of a domain and its set of elements is particularly
important for the definition of equality of domains. Two domains `D` and
`E` are equal (in the sense that

evaluates to `D` = `E``true`

) if and
only if the set of elements of `D` is equal to the set of elements of `E`
(as returned by `Elements( `

and `D` )`Elements( `

). As a special
case either of the operands of `E` )`=`

may also be a proper set, and the
value is `true`

if this set is equal to the set of elements of the
domain.

gap> a4 := Group( (1,2,3), (2,3,4) ); Group( (1,2,3), (2,3,4) ) gap> elms := Elements( a4 ); [ (), (2,3,4), (2,4,3), (1,2)(3,4), (1,2,3), (1,2,4), (1,3,2), (1,3,4), (1,3)(2,4), (1,4,2), (1,4,3), (1,4)(2,3) ] gap> elms = a4; true

However the following example shows that this does not imply that all functions return the same answer for two domains (or a domain and a proper set) that are equal. This is because those function may take the structure into account.

gap> IsGroup( a4 ); true gap> IsGroup( elms ); false gap> Intersection( a4, Group( (1,2), (1,2,3) ) ); Group( (1,2,3) ) gap> Intersection( elms, Group( (1,2), (1,2,3) ) ); [ (), (1,2,3), (1,3,2) ] # this is not a group gap> last = last2; true # but it is equal to the above result gap> Centre( a4 ); Subgroup( Group( (1,2,3), (2,3,4) ), [ ] ) gap> Centre( elms ); Error, <struct> must be a record in Centre( elms ) called from main loop brk> quit;

Generally three things may happen if you have two domains `D` and `E`
that are equal but have different structure (or a domain `D` and a set
`E` that are equal). First a function that tests whether a domain has a
certain structure may return `true`

for `D` and `false`

for `E`. Second
a function may return a domain for `D` and a proper set for `E`. Third a
function may work for `D` and fail for `E`, because it requires the
structure.

A slightly more complex example for the second case is the following.

gap> v4 := Subgroup( a4, [ (1,2)(3,4), (1,3)(2,4) ] ); Subgroup( Group( (1,2,3), (2,3,4) ), [ (1,2)(3,4), (1,3)(2,4) ] ) gap> v4.name := "v4";; gap> rc := v4 * (1,2,3); (v4*(2,4,3)) gap> lc := (1,2,3) * v4; ((1,2,3)*v4) gap> rc = lc; true gap> rc * (1,3,2); (v4*()) gap> lc * (1,3,2); [ (1,3)(2,4), (), (1,2)(3,4), (1,4)(2,3) ] gap> last = last2; false

The two domains `rc`

and `lc`

(yes, cosets are domains too) are equal,
because they have the same set of elements. However if we multiply both
with `(1,3,2)`

we obtain the trivial right coset for `rc`

and a list for
`lc`

. The result for `lc`

is **not** a proper set, because it is not
sorted, therefore `=`

evaluates to `false`

. (For the curious. The
multiplication of a left coset with an element from the right will
generally not yield another coset, i.e., nothing that can easily be
represented as a domain. Thus to multiply `lc`

with `(1,3,2)`

**GAP**
first converts `lc`

to the set of its elements with `Elements`

. But the
definition of multiplication requires that a list `l` multiplied by an
element `e` yields a new list `n` such that each element

in
the new list is the product of the element `n`[`i`]

at the `l`[`i`]**same
position** of the operand list `l` with `e`.)

Note that the above definition only defines **what** the result of the
equality comparison of two domains `D` and `E` should be. It does not
prescribe that this comparison is actually performed by listing all
elements of `D` and `E`. For example, if `D` and `E` are groups, it is
sufficient to check that all generators of `D` lie in `E` and that all
generators of `E` lie in `D`. If **GAP** would really compute the whole
set of elements, the following test could not be performed on any
computer.

gap> Group( (1,2), (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18) ) > = Group( (17,18), (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18) ); true

If we could only apply the set theoretic functions to domains, domains
would be of little use. Luckily this is not so. We already saw that we
could apply `GaloisGroup`

to the finite field with 16 elements, and
`SylowSubgroup`

to the group `a5`

. But those functions are not
applicable to all domains. The argument of `GaloisGroup`

must be a
field, and the argument of `SylowSubgroup`

must be a group.

A **category** is a set of domains. So we say that the argument of
`GaloisGroup`

must be an element of the category of fields, and the
argument of `SylowSubgroup`

must be an element of the category of groups.
The most important categories are **rings**, **fields**, **groups**, and
**vector spaces**. Which category a domain belongs to determines which
functions are applicable to this domain and its elements. We want to
emphasize the each domain belongs to **one and only one** category. This
is necessary because domains in different categories have, sometimes
incompatible, representations.

Note that the categories only exist conceptually. That means that there
is no **GAP** object for the categories, e.g., there is no object
`Groups`

. For each category there exists a function that tests whether a
domain is an element of this category.

gap> IsRing( gf16 ); false gap> IsField( gf16 ); true gap> IsGroup( gf16 ); false gap> IsVectorSpace( gf16 ); false

Note that of course mathematically the field `gf16`

is also a ring and a
vector space. However in **GAP** a domain can only belong to one
category. So a domain is conceptually a set of elements with **one**
structure, e.g., a field structure. That the same set of elements may
also support a different structure, e.g., a ring or vector space
structure, can not be represented by this domain. So you need a
different domain to represent this different structure. (We are planning
to add functions that changes the structure of a domain, e.g. ```
AsRing(
```

should return a new domain with the same elements as `field` )`field`
but with a ring structure.)

Domains may have certain properties. For example a ring may be
commutative and a group may be nilpotent. Whether a domain has a certain
property `Property` can be tested with the function `Is`

.
`Property`

gap> IsCommutativeRing( GaussianIntegers ); true gap> IsNilpotent( a5 ); false

There are also similar functions that test whether a domain (especially a
group) is represented in a certain way. For example `IsPermGroup`

tests
whether a group is represented as a permutation group.

gap> IsPermGroup( a5 ); true gap> IsPermGroup( a4 / v4 ); false # 'a4 / v4' is represented as a generic factor group

There is a slight difference between a function such as `IsNilpotent`

and
a function such as `IsPermGroup`

. The former tests properties of an
**abstract** group and its outcome is independent of the representation of
that group. The latter tests whether a group is given in a certain
representation.

This (rather philosophical) issue is further complicated by the fact that
sometimes representations and properties are not independent. This is
especially subtle with `IsSolvable`

(see IsSolvable) and `IsAgGroup`

(see IsAgGroup). `IsSolvable`

tests whether a group `G` is solvable.
`IsAgGroup`

tests whether a group `G` is represented as a finite
polycyclic group, i.e., by a finite presentation that allows to
Finite Polycyclic Groups). Of course every finite polycyclic group is
solvable, so `IsAgGroup( `

implies `G` )`IsSolvable( `

. On the
other hand `G` )`IsSolvable( `

does not imply `G` )`IsAgGroup( `

,
because, even though each solvable group `G` )**can** be represented as a finite
polycyclic group, it **need** not, e.g., it could also be represented as a
permutation group.

The organization of the manual follows the structure of domains and categories.

After the description of the programming language and the environment chapter Domains describes the domains and the functions applicable to all domains.

Next come the chapters that describe the categories rings, fields, groups, and vector spaces.

The remaining chapters describe **GAP**'s data--types and the domains one
can make with those elements of those data-types. The order of those
chapters roughly follows the order of the categories. The data--types
whose elements form rings and fields come first (e.g., integers and
finite fields), followed by those whose elements form groups (e.g.,
permutations), and so on. The data--types whose elements support little
or no algebraic structure come last (e.g., booleans). In some cases
there may be two chapters for one data--type, one describing the elements
and the other describing the domains made with those elements (e.g.,
permutations and permutation groups).

The **GAP** manual not only describes what you can do, it also gives some
hints how **GAP** performs its computations. However, it can be tricky to
find those hints. The index of this manual can help you.

Suppose that you want to intersect two permutation groups. If you read
the section that describes the function `Intersection`

(see
Intersection) you will see that the last paragraph describes the
default method used by `Intersection`

. Such a last paragraph that
describes the default method is rather typical. In this case it says
that `Intersection`

computes the proper set of elements of both domains
and intersect them. It also says that this method is often overlaid
with a more efficient one. You wonder whether this is the case for
permutation groups. How can you find out? Well you look in the index
under **Intersection**. There you will find a reference **Intersection, for
permutation groups** to section **Set Functions for Permutation Groups**
(see Set Functions for Permutation Groups). This section tells you
that `Intersection`

uses a backtrack for permutation groups (and cites a
book where you can find a description of the backtrack).

Let us now suppose that you intersect two factor groups. There is no
reference in the index for **Intersection, for factor groups**. But there
is a reference for **Intersection, for groups** to the section **Set
Functions for Groups** (see Set Functions for Groups). Since this is
the next best thing, look there. This section further directs you to the
section **Intersection for Groups** (see Intersection for Groups). This
section finally tells you that `Intersection`

computes the intersection
of two groups `G` and `H` as the stabilizer in `G` of the trivial coset
of `H` under the operation of `G` on the right cosets of `H`.

In this section we introduced domains and categories. You have learned that a domain is a structured set, and that domains are either predefined, created by domain constructors, or returned by library functions. You have seen most functions that are applicable to all domains. Those functions generally ignore the structure and treat a domain as the set of its elements. You have learned that categories are sets of domains, and that the category a domain belongs to determines which functions are applicable to this domain.

More information about domains can be found in chapter Domains. Chapters Rings, Fields, Groups, and Vector Spaces define the About the Implementation of Domains opens that black boxes and shows how all this works.

A mapping is an object which maps each element of its source to a value in its range. Source and range can be arbitrary sets of elements. But in most applications the source and range are structured sets and the mapping, in such applications called homomorphism, is compatible with this structure.

In the last sections you have already encountered examples of homomorphisms, namely natural homomorphisms of groups onto their factor groups and operation homomorphisms of groups into symmetric groups.

Finite fields also bear a structure and homomorphisms between fields are always bijections. The Galois group of a finite field is generated by the Frobenius automorphism. It is very easy to construct.

gap> f := FrobeniusAutomorphism( GF(81) ); FrobeniusAutomorphism( GF(3^4) ) gap> Image( f, Z(3^4) ); Z(3^4)^3 gap> A := Group( f ); Group( FrobeniusAutomorphism( GF(3^4) ) ) gap> Size( A ); 4 gap> IsCyclic( A ); true gap> Order( Mappings, f ); 4 gap> Kernel( f ); [ 0*Z(3) ]

For finite fields and cyclotomic fields the function `GaloisGroup`

is an
easy way to construct the Galois group.

gap> GaloisGroup( GF(81) ); Group( FrobeniusAutomorphism( GF(3^4) ) ) gap> Size( last ); 4 gap> GaloisGroup( CyclotomicField( 18 ) ); Group( NFAutomorphism( CF(9) , 2 ) ) gap> Size( last ); 6

Not all group homomorphisms are bijections of course, natural homomorphisms do have a kernel in most cases and operation homomorphisms need neither be surjective nor injective.

gap> s4 := Group( (1,2,3,4), (1,2) ); Group( (1,2,3,4), (1,2) ) gap> s4.name := "s4";; gap> v4 := Subgroup( s4, [ (1,2)(3,4), (1,3)(2,4) ] ); Subgroup( s4, [ (1,2)(3,4), (1,3)(2,4) ] ) gap> v4.name := "v4";; gap> s3 := s4 / v4; (s4 / v4) gap> f := NaturalHomomorphism( s4, s3 ); NaturalHomomorphism( s4, (s4 / v4) ) gap> IsHomomorphism( f ); true gap> IsEpimorphism( f ); true gap> Image( f ); (s4 / v4) gap> IsMonomorphism( f ); false gap> Kernel( f ); v4

The image of a group homomorphism is always one element of the range but
the preimage can be a coset. In order to get one representative of this
coset you can use the function `PreImagesRepresentative`

.

gap> Image( f, (1,2,3,4) ); FactorGroupElement( v4, (2,4) ) gap> PreImages( f, s3.generators[1] ); (v4*(2,4)) gap> PreImagesRepresentative( f, s3.generators[1] ); (2,4)

But even if the homomorphism is a monomorphism but not surjective you can
use the function `PreImagesRepresentative`

in order to get the preimage
of an element of the range.

gap> A := Z(3) * [ [ 0, 1 ], [ 1, 0 ] ];; gap> B := Z(3) * [ [ 0, 1 ], [ -1, 0 ] ];; gap> G := Group( A, B ); Group( [ [ 0*Z(3), Z(3) ], [ Z(3), 0*Z(3) ] ], [ [ 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3) ] ] ) gap> Size( G ); 8 gap> G.name := "G";; gap> d8 := Operation( G, Orbit( G, Z(3)*[1,0] ) ); Group( (1,2)(3,4), (1,2,3,4) ) gap> e := OperationHomomorphism( Subgroup( G, [B] ), d8 ); OperationHomomorphism( Subgroup( G, [ [ [ 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3) ] ] ] ), Group( (1,2)(3,4), (1,2,3,4) ) ) gap> Kernel( e ); Subgroup( G, [ ] ) gap> IsSurjective( e ); false gap> PreImages( e, (1,3)(2,4) ); (Subgroup( G, [ ] )*[ [ Z(3), 0*Z(3) ], [ 0*Z(3), Z(3) ] ]) gap> PreImage( e, (1,3)(2,4) ); Error, <bij> must be a bijection, not an arbitrary mapping in bij.operations.PreImageElm( bij, img ) called from PreImage( e, (1,3)(2,4) ) called from main loop brk> quit; gap> PreImagesRepresentative( e, (1,3)(2,4) ); [ [ Z(3), 0*Z(3) ], [ 0*Z(3), Z(3) ] ]

Only bijections allow `PreImage`

in order to get the preimage of an
element of the range.

gap> Operation( G, Orbit( G, Z(3)*[1,0] ) ); Group( (1,2)(3,4), (1,2,3,4) ) gap> d := OperationHomomorphism( G, last ); OperationHomomorphism( G, Group( (1,2)(3,4), (1,2,3,4) ) ) gap> PreImage( d, (1,3)(2,4) ); [ [ Z(3), 0*Z(3) ], [ 0*Z(3), Z(3) ] ]

Both `PreImage`

and `PreImages`

can also be applied to sets. They return
the complete preimage.

gap> PreImages( d, Group( (1,2)(3,4), (1,3)(2,4) ) ); Subgroup( G, [ [ [ 0*Z(3), Z(3) ], [ Z(3), 0*Z(3) ] ], [ [ Z(3), 0*Z(3) ], [ 0*Z(3), Z(3) ] ] ] ) gap> Size( last ); 4 gap> f := NaturalHomomorphism( s4, s3 ); NaturalHomomorphism( s4, (s4 / v4) ) gap> PreImages( f, s3 ); Subgroup( s4, [ (1,2)(3,4), (1,3)(2,4), (2,4), (3,4) ] ) gap> Size( last ); 24

Another way to construct a group automorphism is to use elements in the normalizer of a subgroup and construct the induced automorphism. A special case is the inner automorphism induced by an element of a group, a more general case is a surjective homomorphism induced by arbitrary elements of the parent group.

gap> d12 := Group((1,2,3,4,5,6),(2,6)(3,5));; d12.name := "d12";; gap> i1 := InnerAutomorphism( d12, (1,2,3,4,5,6) ); InnerAutomorphism( d12, (1,2,3,4,5,6) ) gap> Image( i1, (2,6)(3,5) ); (1,3)(4,6) gap> IsAutomorphism( i1 ); true

Mappings can also be multiplied, provided that the range of the first
mapping is a subgroup of the source of the second mapping. The
multiplication is of course defined as the composition. Note that, in
line with the fact that mappings operate **from the right**, `Image( `

is defined as `map1`
* `map2`, `elm` )`Image( `

.
`map2`, Image( `map1`, `elm` )
)

gap> i2 := InnerAutomorphism( d12, (2,6)(3,5) ); InnerAutomorphism( d12, (2,6)(3,5) ) gap> i1 * i2; InnerAutomorphism( d12, (1,6)(2,5)(3,4) ) gap> Image( last, (2,6)(3,5) ); (1,5)(2,4)

Mappings can also be inverted, provided that they are bijections.

gap> i1 ^ -1; InnerAutomorphism( d12, (1,6,5,4,3,2) ) gap> Image( last, (2,6)(3,5) ); (1,5)(2,4)

Whenever you have a set of bijective mappings on a finite set (or domain)
you can construct the group generated by those mappings. So in the
following example we create the group of inner automorphisms of `d12`

.

gap> autd12 := Group( i1, i2 ); Group( InnerAutomorphism( d12, (1,2,3,4,5,6) ), InnerAutomorphism( d12, (2,6)(3,5) ) ) gap> Size( autd12 ); 6 gap> Index( d12, Centre( d12 ) ); 6

Note that the computation with such automorphism groups in their present implementation is not very efficient. For example to compute the size of such an automorphism group all elements are computed. Thus work with such automorphism groups should be restricted to very small examples.

The function `ConjugationGroupHomomorphism`

is a generalization of
`InnerAutomorphism`

. It accepts a source and a range and an element that
conjugates the source into the range. Source and range must lie in a
common parent group, and the conjugating element must also lie in this
parent group.

gap> c2 := Subgroup( d12, [ (2,6)(3,5) ] ); Subgroup( d12, [ (2,6)(3,5) ] ) gap> v4 := Subgroup( d12, [ (1,2)(3,6)(4,5), (1,4)(2,5)(3,6) ] ); Subgroup( d12, [ (1,2)(3,6)(4,5), (1,4)(2,5)(3,6) ] ) gap> x := ConjugationGroupHomomorphism( c2, v4, (1,3,5)(2,4,6) ); ConjugationGroupHomomorphism( Subgroup( d12, [ (2,6)(3,5) ] ), Subgroup( d12, [ (1,2)(3,6)(4,5), (1,4)(2,5)(3,6) ] ), (1,3,5)(2,4,6) ) gap> IsSurjective( x ); false gap> Image( x ); Subgroup( d12, [ (1,5)(2,4) ] )

But how can we construct homomorphisms which are not induced by elements of the parent group? The most general way to construct a group homomorphism is to define the source, range and the images of the generators under the homomorphism in mind.

gap> c := GroupHomomorphismByImages( G, s4, [A,B], [(1,2),(3,4)] ); GroupHomomorphismByImages( G, s4, [ [ [ 0*Z(3), Z(3) ], [ Z(3), 0*Z(3) ] ], [ [ 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3) ] ] ], [ (1,2), (3,4) ] ) gap> Kernel( c ); Subgroup( G, [ [ [ Z(3), 0*Z(3) ], [ 0*Z(3), Z(3) ] ] ] ) gap> Image( c ); Subgroup( s4, [ (1,2), (3,4) ] ) gap> IsHomomorphism( c ); true gap> Image( c, A ); (1,2) gap> PreImages( c, (1,2) ); (Subgroup( G, [ [ [ Z(3), 0*Z(3) ], [ 0*Z(3), Z(3) ] ] ] )* [ [ 0*Z(3), Z(3) ], [ Z(3), 0*Z(3) ] ])

Note that it is possible to construct a general mapping this way that is
not a homomorphism, because `GroupHomomorphismByImages`

does not check if
the given images fulfill the relations of the generators.

gap> b := GroupHomomorphismByImages( G, s4, [A,B], [(1,2,3),(3,4)] ); GroupHomomorphismByImages( G, s4, [ [ [ 0*Z(3), Z(3) ], [ Z(3), 0*Z(3) ] ], [ [ 0*Z(3), Z(3) ], [ Z(3)^0, 0*Z(3) ] ] ], [ (1,2,3), (3,4) ] ) gap> IsHomomorphism( b ); false gap> Images( b, A ); (Subgroup( s4, [ (1,3,2), (2,3,4), (1,3,4), (1,4)(2,3), (1,4,2) ] )*())

The result is a **multi valued mapping**, i.e., one that maps each element
of its source to a set of elements in its range. The set of images of
`A`

under `b`

is defined as follows. Take all the words of two letters
*w( x, y )* such that *w( A, B ) = A*, e.g., *x* and *x y x y x*. Then
the set of images is the set of elements that you get by inserting the
images of `A`

and `B`

in those words, i.e., *w( (1,2,3), (3,4) )*, e.g.,
*(1,2,3)* and *(1,4,2)*. One can show that the set of images of the
identity under a multi valued mapping such as `b`

is a subgroup and that
the set of images of other elements are cosets of this subgroup.

defcolonchar58

This section contains some examples of the use of **GAP** in character
theory. First a few very simple commands for handling character tables
are introduced, and afterwards we will construct the character tables of
*(A_5times 3)!colon!2* and of *A_6.2^2*.

**GAP** has a large library of character tables, so let us look at one of
these tables, e.g., the table of the Mathieu group *M_{11}*:

gap> m11:= CharTable( "M11" ); CharTable( "M11" )

Character tables contain a lot of information. This is not printed in full length since the internal structure is not easy to read. The next statement shows a more comfortable output format.

gap> DisplayCharTable( m11 ); M112 4 4 1 3 . 1 3 3 . . 3 2 1 2 . . 1 . . . . 5 1 . . . 1 . . . . . 11 1 . . . . . . . 1 1

1a 2a 3a 4a 5a 6a 8a 8b 11a 11b 2P 1a 1a 3a 2a 5a 3a 4a 4a 11b 11a 3P 1a 2a 1a 4a 5a 2a 8a 8b 11a 11b 5P 1a 2a 3a 4a 1a 6a 8b 8a 11a 11b 11P 1a 2a 3a 4a 5a 6a 8a 8b 1a 1a

X.1 1 1 1 1 1 1 1 1 1 1 X.2 10 2 1 2 . -1 . . -1 -1 X.3 10 -2 1 . . 1 A -A -1 -1 X.4 10 -2 1 . . 1 -A A -1 -1 X.5 11 3 2 -1 1 . -1 -1 . . X.6 16 . -2 . 1 . . . B /B X.7 16 . -2 . 1 . . . /B B X.8 44 4 -1 . -1 1 . . . . X.9 45 -3 . 1 . . -1 -1 1 1 X.10 55 -1 1 -1 . -1 1 1 . .

A = E(8)+E(8)^3 = ER(-2) = i2 B = E(11)+E(11)^3+E(11)^4+E(11)^5+E(11)^9 = (-1+ER(-11))/2 = b11

We are not too much interested in the internal structure of this
character table (see Character Table Records); but of course we can
access all information about the centralizer orders (first four lines),
element orders (next line), power maps for the prime divisors of the
group order (next four lines), irreducible characters (lines parametrized
by `X.1`

ldots `X.10`

) and irrational character values (last four
lines), see DisplayCharTable for a detailed description of the format
of the displayed table. E.g., the irreducible characters are a list with
name `m11.irreducibles`

, and each character is a list of cyclotomic
integers (see chapter Cyclotomics). There are various ways to describe
the irrationalities; e.g., the square root of *-2* can be entered as
`E(8) + E(8)^3`

or `ER(-2)`

, the famous ATLAS of Finite
Groups~CCN85 denotes it as `i2`

.

gap> m11.irreducibles[3]; [ 10, -2, 1, 0, 0, 1, E(8)+E(8)^3, -E(8)-E(8)^3, -1, -1 ]

We can for instance form tensor products of this character with all irreducibles, and compute the decomposition into irreducibles.

gap> tens:= Tensored( [ last ], m11.irreducibles );; gap> MatScalarProducts( m11, m11.irreducibles, tens ); [ [ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0 ], [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 ], [ 0, 0, 0, 0, 1, 0, 0, 1, 1, 0 ], [ 1, 0, 0, 0, 0, 0, 0, 1, 0, 1 ], [ 0, 0, 0, 1, 0, 0, 0, 0, 1, 1 ], [ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1 ], [ 0, 0, 0, 0, 0, 1, 0, 1, 1, 1 ], [ 0, 0, 1, 1, 0, 1, 1, 2, 3, 3 ], [ 0, 1, 0, 1, 1, 1, 1, 3, 2, 3 ], [ 0, 1, 1, 0, 1, 1, 1, 3, 3, 4 ] ]

The decomposition means for example that the third character in the list
`tens`

is the sum of the irreducible characters at positions 5, 8 and 9.

gap> tens[3]; [ 100, 4, 1, 0, 0, 1, -2, -2, 1, 1 ] gap> tens[3] = Sum( Sublist( m11.irreducibles, [ 5, 8, 9 ] ) ); true

Or we can compute symmetrizations, e.g., the characters *chi^{2+}*,
defined by *chi^{2+}(g) = frac{1}{2} ( chi^2(g) + chi(g^2) )*, for
all irreducibles.

gap> sym:= SymmetricParts( m11, m11.irreducibles, 2 );; gap> MatScalarProducts( m11, m11.irreducibles, sym ); [ [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [ 1, 1, 0, 0, 0, 0, 0, 1, 0, 0 ], [ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0 ], [ 0, 0, 0, 0, 1, 0, 0, 1, 0, 0 ], [ 1, 1, 0, 0, 1, 0, 0, 1, 0, 0 ], [ 0, 1, 0, 0, 1, 0, 1, 1, 0, 1 ], [ 0, 1, 0, 0, 1, 1, 0, 1, 0, 1 ], [ 1, 3, 0, 0, 3, 2, 2, 8, 4, 6 ], [ 1, 2, 0, 0, 3, 2, 2, 8, 4, 7 ], [ 1, 3, 1, 1, 4, 3, 3, 11, 7, 10 ] ] gap> sym[2]; [ 55, 7, 1, 3, 0, 1, 1, 1, 0, 0 ] gap> sym[2] = Sum( Sublist( m11.irreducibles, [ 1, 2, 8 ] ) ); true

If the subgroup fusion into a supergroup is known, characters can be
induced to this group, e.g., to obtain the permutation character of the
action of *M_{12}* on the cosets of *M_{11}*.

gap> m12:= CharTable( "M12" );; gap> permchar:= Induced( m11, m12, [ m11.irreducibles[1] ] ); [ [ 12, 0, 4, 3, 0, 0, 4, 2, 0, 1, 0, 2, 0, 1, 1 ] ] gap> MatScalarProducts( m12, m12.irreducibles, last ); [ [ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ] gap> DisplayCharTable( m12, rec( chars:= permchar ) ); M122 6 4 6 1 2 5 5 1 2 1 3 3 1 . . 3 3 1 1 3 2 . . . 1 1 . . . . . 5 1 1 . . . . . 1 . . . . 1 . . 11 1 . . . . . . . . . . . . 1 1

1a 2a 2b 3a 3b 4a 4b 5a 6a 6b 8a 8b 10a 11a 11b 2P 1a 1a 1a 3a 3b 2b 2b 5a 3b 3a 4a 4b 5a 11b 11a 3P 1a 2a 2b 1a 1a 4a 4b 5a 2a 2b 8a 8b 10a 11a 11b 5P 1a 2a 2b 3a 3b 4a 4b 1a 6a 6b 8a 8b 2a 11a 11b 11P 1a 2a 2b 3a 3b 4a 4b 5a 6a 6b 8a 8b 10a 1a 1a

Y.1 12 . 4 3 . . 4 2 . 1 . 2 . 1 1

It should be emphasized that the heart of character theory is dealing
with lists. Characters are lists, and also the maps which occur are
represented as lists. Note that the multiplication of group elements is
not available, so we neither have homomorphisms. All we can talk of are
class functions, and the lists are regarded as such functions, being the
lists of images with respect to a fixed order of conjugacy classes.
Therefore we do not write `chi( cl )`

or `cl^chi`

for the value of the
character `chi`

on the class `cl`

, but `chi[i]`

where `i`

is the position
of the class `cl`

.

Since the data structures are so basic, most calculations involve
compositions of maps; for example, the embedding of a subgroup in a group
is described by the so--called subgroup fusion which is a class function
that maps each class *c* of the subgroup to that class of the group that
contains *c*. Consider the symmetric group *S_5 cong A_5.2* as subgroup
of *M_{11}*. (Do not worry about the names that are used to get library
tables, see CharTable for an overview.)

gap> s5:= CharTable( "A5.2" );; gap> map:= GetFusionMap( s5, m11 ); [ 1, 2, 3, 5, 2, 4, 6 ]

The subgroup fusion is already stored on the table. We see that class 1
of `s5`

is mapped to class 1 of `m11`

(which means that the identity of
*S_5* maps to the identity of *M_{11}*), classes 2 and 5 of `s5`

both map
to class 2 of `m11`

(which means that all involutions of *S_5* are
conjugate in *M_{11}*), and so on.

The restriction of a character of `m11`

to `s5`

is just the composition
of this character with the subgroup fusion map. Viewing this map as list
one would call this composition an indirection.

gap> chi:= m11.irreducibles[3]; [ 10, -2, 1, 0, 0, 1, E(8)+E(8)^3, -E(8)-E(8)^3, -1, -1 ] gap> rest:= List( map, x -> chi[x] ); [ 10, -2, 1, 0, -2, 0, 1 ]

This looks very easy, and many **GAP** functions in character theory do
such simple calculations. But note that it is not always obvious that a
list is regarded as a map, where preimages and/or images refer to
positions of certain conjugacy classes.

gap> alt:= s5.irreducibles[2]; [ 1, 1, 1, 1, -1, -1, -1 ] gap> kernel:= KernelChar( last ); [ 1, 2, 3, 4 ]

The kernel of a character is represented as the list of (positions of)
classes lying in the kernel. We know that the kernel of the alternating
character `alt`

of `s5`

is the alternating group *A_5*. The order of the
kernel can be computed as sum of the lengths of the contained classes
from the character table, using that the classlengths are stored in the
`classes`

component of the table.

gap> s5.classes; [ 1, 15, 20, 24, 10, 30, 20 ] gap> last{ kernel }; [ 1, 15, 20, 24 ] gap> Sum( last ); 60

We chose those classlengths of `s5`

that belong to the *S_5*--classes
contained in the alternating group. The same thing is done in the
following command, reflecting the view of the kernel as map.

gap> List( kernel, x -> s5.classes[x] ); [ 1, 15, 20, 24 ] gap> Sum( kernel, x -> s5.classes[x] ); 60

This small example shows how the functions `List`

and `Sum`

can be used.
About Further List Operations, and we will make heavy use of them; in many cases such
a command might look very strange, but it is just the translation of a
(hardly less complicated) mathematical formula to character theory.

And now let us construct some small character tables!

setlengthunitlength0.1cm
The group *G = (A_5times 3)!colon!2* is a maximal subgroup of the
alternating group *A_8*; *G* extends to *S_5times S_3* in *S_8*. We
want to construct the character table of *G*.

First the tables of the subgroup *A_5times 3* and the supergroup
*S_5times S_3* are constructed; the tables of the factors of each direct
product are again got from the table library using admissible names, see
CharTable for this.
put(10,-37)beginpicture(50,45)
put(25,5)circle1
put(15,15)circle1
put(25,25)circle1
put(25,30)circle1
put(25,35)circle1
put(35,15)circle1
put(10,20)circle1
put(40,20)circle1
put(20,30)circle1
put(30,30)circle1
put(7,20)makebox(0,0)*S_5*
put(12,15)makebox(0,0)*A_5*
put(43,20)makebox(0,0)*S_3*
put(38,15)makebox(0,0)*3*
put(27,30)makebox(0,0)*G*
put(25,37)makebox(0,0)*S_5times S_3*
put(14,30)makebox(0,0)*S_5times 3*
put(38,30)makebox(0,0)*A_5times S_3*
put(25,5)line(-1,1)15
put(25,5)line(1,1)15
put(35,15)line(-1,1)15
put(15,15)line(1,1)15
put(40,20)line(-1,1)15
put(10,20)line(1,1)15
put(25,25)line(0,1)10

gap> a5:= CharTable( "A5" );; gap> c3:= CharTable( "Cyclic", 3 );; gap> a5xc3:= CharTableDirectProduct( a5, c3 );; gap> s5:= CharTable( "A5.2" );; gap> s3:= CharTable( "Symmetric", 3 );; gap> s3.irreducibles; [ [ 1, -1, 1 ], [ 2, 0, -1 ], [ 1, 1, 1 ] ] # The trivial character shall be the first one. gap> SortCharactersCharTable( s3 ); # returns the applied permutation (1,2,3) gap> s5xs3:= CharTableDirectProduct( s5, s3 );;

*G* is the normal subgroup of index 2 in *S_5times S_3* which contains
neither *S_5* nor the normal *S_3*. We want to find the classes of
`s5xs3`

whose union is *G*. For that, we compute the set of kernels of
irreducibles --remember that they are given simply by lists of numbers of
contained classes-- and then choose those kernels belonging to normal
subgroups of index 2.

gap> kernels:= Set( List( s5xs3.irreducibles, KernelChar ) ); [ [ 1 ], [ 1, 2, 3 ], [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ], [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21 ], [ 1, 3 ], [ 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21 ], [ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ], [ 1, 4, 7, 10 ], [ 1, 4, 7, 10, 13, 16, 19 ] ] gap> sizes:= List( kernels, x -> Sum( Sublist( s5xs3.classes, x ) ) ); [ 1, 6, 360, 720, 3, 360, 360, 60, 120 ] gap> s5xs3.size; 720 gap> index2:= Sublist( kernels, [ 3, 6, 7 ] ); [ [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ], [ 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21 ], [ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ] ]

In order to decide which kernel describes *G*, we consider the embeddings
of `s5`

and `s3`

in `s5xs3`

, given by the subgroup fusions.

gap> s5ins5xs3:= GetFusionMap( s5, s5xs3 ); [ 1, 4, 7, 10, 13, 16, 19 ] gap> s3ins5xs3:= GetFusionMap( s3, s5xs3 ); [ 1, 2, 3 ] gap> Filtered( index2, x->Intersection(x,s5ins5xs3)<>s5ins5xs3 and > Intersection(x,s3ins5xs3)<>s3ins5xs3 ); [ [ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ] ] gap> nsg:= last[1]; [ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ]

We now construct a first approximation of the character table of this
normal subgroup, namely the restriction of `s5xs3`

to the classes given
by `nsg`

.

gap> sub:= CharTableNormalSubgroup( s5xs3, nsg );; #I CharTableNormalSubgroup: classes in [ 8 ] necessarily split gap> PrintCharTable( sub ); rec( identifier := "Rest(A5.2xS3,[ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 2\ 0 ])", size := 360, name := "Rest(A5.2xS3,[ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ])",\ order := 360, centralizers := [ 360, 180, 24, 12, 18, 9, 15, 15/2, 12, 4, 6 ], orders := [ 1, 3, 2, 6, 3, 3, 5, 15, 2, 4, 6 ], powermap := [ , [ 1, 2, 1, 2, 5, 6, 7, 8, 1, 3, 5 ], [ 1, 1, 3, 3, 1, 1, 7, 7, 9, 10, 9 ],, [ 1, 2, 3, 4, 5, 6, 1, 2, 9, 10, 11 ] ], classes := [ 1, 2, 15, 30, 20, 40, 24, 48, 30, 90, 60 ], operations := CharTableOps, irreducibles := [ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1 ], [ 2, -1, 2, -1, 2, -1, 2, -1, 0, 0, 0 ], [ 6, 6, -2, -2, 0, 0, 1, 1, 0, 0, 0 ], [ 4, 4, 0, 0, 1, 1, -1, -1, 2, 0, -1 ], [ 4, 4, 0, 0, 1, 1, -1, -1, -2, 0, 1 ], [ 8, -4, 0, 0, 2, -1, -2, 1, 0, 0, 0 ], [ 5, 5, 1, 1, -1, -1, 0, 0, 1, -1, 1 ], [ 5, 5, 1, 1, -1, -1, 0, 0, -1, 1, -1 ], [ 10, -5, 2, -1, -2, 1, 0, 0, 0, 0, 0 ] ], fusions := [ rec( name := [ 'A', '5', '.', '2', 'x', 'S', '3' ], map := [ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ] ) ] )

Not all restrictions of irreducible characters of `s5xs3`

to `sub`

remain
irreducible. We compute those restrictions with norm larger than 1.

gap> red:= Filtered( Restricted( s5xs3, sub, s5xs3.irreducibles ), > x -> ScalarProduct( sub, x, x ) > 1 ); [ [ 12, -6, -4, 2, 0, 0, 2, -1, 0, 0, 0 ] ] gap> Filtered( [ 1 .. Length( nsg ) ], > x -> not IsInt( sub.centralizers[x] ) ); [ 8 ]

Note that `sub`

is not actually a character table in the sense of
mathematics but only a record with components like a character table.
**GAP** does not know about this subtleties and treats it as a character
table.

As the list `centralizers`

of centralizer orders shows, at least class 8
splits into two conjugacy classes in *G*, since this is the only
possibility to achieve integral centralizer orders.

Since 10 restrictions of irreducible characters remain irreducible for
*G* (`sub`

contains 10 irreducibles), only one of the 11 irreducibles of
*S_5times S_3* splits into two irreducibles of *G*, in other words,
class 8 is the only splitting class.

Thus we create a new approximation of the desired character table (which
we call `split`

) where this class is split; 8th and 9th column of the
known irreducibles are of course equal, and due to the splitting the
second powermap for these columns is ambiguous.

gap> splitting:= [ 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 10, 11 ];; gap> split:= CharTableSplitClasses( sub, splitting );; gap> PrintCharTable( split ); rec( identifier := "Split(Rest(A5.2xS3,[ 1, 3, 4, 6, 7, 9, 10, 12, 14,\ 17, 20 ]),[ 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 10, 11 ])", size := 360, order := 360, name := "Split(Rest(A5.2xS3,[ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 2\ 0 ]),[ 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 10, 11 ])", centralizers := [ 360, 180, 24, 12, 18, 9, 15, 15, 15, 12, 4, 6 ], classes := [ 1, 2, 15, 30, 20, 40, 24, 24, 24, 30, 90, 60 ], orders := [ 1, 3, 2, 6, 3, 3, 5, 15, 15, 2, 4, 6 ], powermap := [ , [ 1, 2, 1, 2, 5, 6, 7, [ 8, 9 ], [ 8, 9 ], 1, 3, 5 ], [ 1, 1, 3, 3, 1, 1, 7, 7, 7, 10, 11, 10 ],, [ 1, 2, 3, 4, 5, 6, 1, 2, 2, 10, 11, 12 ] ], irreducibles := [ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1 ], [ 2, -1, 2, -1, 2, -1, 2, -1, -1, 0, 0, 0 ], [ 6, 6, -2, -2, 0, 0, 1, 1, 1, 0, 0, 0 ], [ 4, 4, 0, 0, 1, 1, -1, -1, -1, 2, 0, -1 ], [ 4, 4, 0, 0, 1, 1, -1, -1, -1, -2, 0, 1 ], [ 8, -4, 0, 0, 2, -1, -2, 1, 1, 0, 0, 0 ], [ 5, 5, 1, 1, -1, -1, 0, 0, 0, 1, -1, 1 ], [ 5, 5, 1, 1, -1, -1, 0, 0, 0, -1, 1, -1 ], [ 10, -5, 2, -1, -2, 1, 0, 0, 0, 0, 0, 0 ] ], fusions := [ rec( name := "Rest(A5.2xS3,[ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ])" , map := [ 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 10, 11 ] ) ], operations := CharTableOps ) gap> Restricted( sub, split, red ); [ [ 12, -6, -4, 2, 0, 0, 2, -1, -1, 0, 0, 0 ] ]

To complete the table means to find the missing two irreducibles and to
complete the powermaps. For this, there are different possibilities.
First, one can try to embed *G* in *A_8*.

gap> a8:= CharTable( "A8" );; gap> fus:= SubgroupFusions( split, a8 ); [ [ 1, 4, 3, 9, 4, 5, 8, 13, 14, 3, 7, 9 ], [ 1, 4, 3, 9, 4, 5, 8, 14, 13, 3, 7, 9 ] ] gap> fus:= RepresentativesFusions( split, fus, a8 ); #I RepresentativesFusions: no subtable automorphisms stored [ [ 1, 4, 3, 9, 4, 5, 8, 13, 14, 3, 7, 9 ] ] gap> StoreFusion( split, a8, fus[1] );

The subgroup fusion is unique up to table automorphisms. Now we restrict
the irreducibles of *A_8* to *G* and reduce.

gap> rest:= Restricted( a8, split, a8.irreducibles );; gap> red:= Reduced( split, split.irreducibles, rest ); rec( remainders := [ ], irreducibles := [ [ 6, -3, -2, 1, 0, 0, 1, -E(15)-E(15)^2-E(15)^4-E(15)^8, -E(15)^7-E(15)^11-E(15)^13-E(15)^14, 0, 0, 0 ], [ 6, -3, -2, 1, 0, 0, 1, -E(15)^7-E(15)^11-E(15)^13-E(15)^14, -E(15)-E(15)^2-E(15)^4-E(15)^8, 0, 0, 0 ] ] ) gap> Append( split.irreducibles, red.irreducibles );

The list of irreducibles is now complete, but the powermaps are not yet
adjusted. To complete the 2nd powermap, we transfer that of *A_8* to *G*
using the subgroup fusion.

gap> split.powermap; [ , [ 1, 2, 1, 2, 5, 6, 7, [ 8, 9 ], [ 8, 9 ], 1, 3, 5 ], [ 1, 1, 3, 3, 1, 1, 7, 7, 7, 10, 11, 10 ],, [ 1, 2, 3, 4, 5, 6, 1, 2, 2, 10, 11, 12 ] ] gap> TransferDiagram( split.powermap[2], fus[1], a8.powermap[2] );;

And this is the complete table.

gap> split.identifier:= "(A5x3):2";; gap> DisplayCharTable( split ); Split(Rest(A5.2xS3,[ 1, 3, 4, 6, 7, 9, 10, 12, 14, 17, 20 ]),[ 1, 2, 3\ , 4, 5, 6, 7, 8, 8, 9, 10, 11 ])2 3 2 3 2 1 . . . . 2 2 1 3 2 2 1 1 2 2 1 1 1 1 . 1 5 1 1 . . . . 1 1 1 . . .

1a 3a 2a 6a 3b 3c 5a 15a 15b 2b 4a 6b 2P 1a 3a 1a 3a 3b 3c 5a 15a 15b 1a 2a 3b 3P 1a 1a 2a 2a 1a 1a 5a 5a 5a 2b 4a 2b 5P 1a 3a 2a 6a 3b 3c 1a 3a 3a 2b 4a 6b

X.1 1 1 1 1 1 1 1 1 1 1 1 1 X.2 1 1 1 1 1 1 1 1 1 -1 -1 -1 X.3 2 -1 2 -1 2 -1 2 -1 -1 . . . X.4 6 6 -2 -2 . . 1 1 1 . . . X.5 4 4 . . 1 1 -1 -1 -1 2 . -1 X.6 4 4 . . 1 1 -1 -1 -1 -2 . 1 X.7 8 -4 . . 2 -1 -2 1 1 . . . X.8 5 5 1 1 -1 -1 . . . 1 -1 1 X.9 5 5 1 1 -1 -1 . . . -1 1 -1 X.10 10 -5 2 -1 -2 1 . . . . . . X.11 6 -3 -2 1 . . 1 A /A . . . X.12 6 -3 -2 1 . . 1 /A A . . .

A = -E(15)-E(15)^2-E(15)^4-E(15)^8 = (-1-ER(-15))/2 = -1-b15

There are many ways around the block, so two further methods to complete
the table `split`

shall be demonstrated; but we will not go into details.

Without use of **GAP** one could work as follows:

The irrationalities --and there must be irrational entries in the
character table of *G*, since the outer 2 can conjugate at most two of
the four Galois conjugate classes of elements of order 15-- could also
have been found from the structure of *G* and the restriction of the
irreducible *S_5times S_3* character of degree 12.

On the classes that did not split the values of this character must just
be divided by 2. Let *x* be one of the irrationalities. The second
orthogonality relation tells us that *xcdotoverline{x} = 4* (at class
`15a`

) and *x + xast = -1* (at classes `1a`

and `15a`

); here *xast*
denotes the nontrivial Galois conjugate of *x*. This has no solution for
*x = overline{x}*, otherwise it leads to the quadratic equation
*x^2+x+4 = 0* with solutions *b15 = frac{1}{2}(-1+sqrt{-15})* and
*-1-b15*.

The third possibility to complete the table is to embed *A_5times 3*:

gap> split.irreducibles := split.irreducibles{ [ 1 .. 10 ] };; gap> SubgroupFusions( a5xc3, split ); [ [ 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, [ 8, 9 ], [ 8, 9 ], 7, [ 8, 9 ], [ 8, 9 ] ] ]

The images of the four classes of element order 15 are not determined,
the returned list parametrizes the *2^4* possibilities.

gap> fus:= ContainedMaps( last[1] );; gap> Length( fus ); 16 gap> fus[1]; [ 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 7, 8, 8 ]

Most of these 16 possibilities are excluded using scalar products of
induced characters. We take a suitable character `chi`

of `a5xc3`

and
compute the norm of the induced character with respect to each possible
map.

gap> chi:= a5xc3.irreducibles[5]; [ 3, 3*E(3), 3*E(3)^2, -1, -E(3), -E(3)^2, 0, 0, 0, -E(5)-E(5)^4, -E(15)^2-E(15)^8, -E(15)^7-E(15)^13, -E(5)^2-E(5)^3, -E(15)^11-E(15)^14, -E(15)-E(15)^4 ] gap> List( fus, x -> List( Induced( a5xc3, split, [ chi ], x ), > y -> ScalarProduct( split, y, y ) )[1] ); [ 8/15, -2/3*E(5)-11/15*E(5)^2-11/15*E(5)^3-2/3*E(5)^4, -2/3*E(5)-11/15*E(5)^2-11/15*E(5)^3-2/3*E(5)^4, 2/3, -11/15*E(5)-2/3*E(5)^2-2/3*E(5)^3-11/15*E(5)^4, 3/5, 1, -11/15*E(5)-2/3*E(5)^2-2/3*E(5)^3-11/15*E(5)^4, -11/15*E(5)-2/3*E(5)^2-2/3*E(5)^3-11/15*E(5)^4, 1, 3/5, -11/15*E(5)-2/3*E(5)^2-2/3*E(5)^3-11/15*E(5)^4, 2/3, -2/3*E(5)-11/15*E(5)^2-11/15*E(5)^3-2/3*E(5)^4, -2/3*E(5)-11/15*E(5)^2-11/15*E(5)^3-2/3*E(5)^4, 8/15 ] gap> Filtered( [ 1 .. Length( fus ) ], x -> IsInt( last[x] ) ); [ 7, 10 ]

So only fusions 7 and 10 may be possible. They are equivalent (with
respect to table automorphisms), and the list of induced characters
contains the missing irreducibles of *G*:

gap> Sublist( fus, last ); [ [ 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 9, 7, 9, 8 ], [ 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 9, 8, 7, 8, 9 ] ] gap> ind:= Induced( a5xc3, split, a5xc3.irreducibles, last[1] );; gap> Reduced( split, split.irreducibles, ind ); rec( remainders := [ ], irreducibles := [ [ 6, -3, -2, 1, 0, 0, 1, -E(15)-E(15)^2-E(15)^4-E(15)^8, -E(15)^7-E(15)^11-E(15)^13-E(15)^14, 0, 0, 0 ], [ 6, -3, -2, 1, 0, 0, 1, -E(15)^7-E(15)^11-E(15)^13-E(15)^14, -E(15)-E(15)^2-E(15)^4-E(15)^8, 0, 0, 0 ] ] )

The following example is thought mainly for experts. It shall
demonstrate how one can work together with **GAP** and the
**ATLAS**~CCN85, so better leave out the rest of this section if
you are not familiar with the **ATLAS**.

setlengthunitlength0.1cm
We shall construct the character table of the group
*G = A_6.2^2 cong Aut( A_6 )* from the tables of the normal subgroups
*A_6.2_1 cong S_6*, *A_6.2_2 cong PGL(2,9)* and *A_6.2_3 cong M_{10}*.

We regard *G* as a downward extension of the Klein fourgroup *2^2* with
*A_6*. The set of classes of all preimages of cyclic subgroups of *2^2*
covers the classes of *G*, but it may happen that some representatives
are conjugate in *G*, i.e., the classes fuse.

The **ATLAS** denotes the character tables of *G*, *G.2_1*, *G.2_2* and
*G.2_3* as follows:
put(5,-40)beginpicture(50,45)
put(25,5)circle1
put(25,20)makebox(0,0)*A_6*
put(25,30)makebox(0,0)*A_6.2_3*
put(15,30)makebox(0,0)*A_6.2_1*
put(35,30)makebox(0,0)*A_6.2_2*
put(25,40)makebox(0,0)*G*
put(25,5)line(0,1)12
put(23,22)line(-1,1)6
put(27,22)line(1,1)6
put(25,22)line(0,1)6
put(23,38)line(-1,-1)6
put(27,38)line(1,-1)6
put(25,38)line(0,-1)6

vbox

; @ @ @ @ @ @ @ ; ; @ @ @ @ @mboxttchar13360 8 9 9 4 5 5 24 24 4 3 3 p power A A A A A A A A A AB BC p

part A A A A A A A A A AB BC ind 1A 2A 3A 3B 4A 5A B* fus ind 2B 2C 4B 6A 6B

+ 1 1 1 1 1 1 1 : ++ 1 1 1 1 1

+ 5 1 2 -1 -1 0 0 : ++ 3 -1 1 0 -1

+ 5 1 -1 2 -1 0 0 : ++ -1 3 1 -1 0

+ 8 0 -1 -1 0 -b5 * . + 0 0 0 0 0

+ 8 0 -1 -1 0 * -b5 .

+ 9 1 0 0 1 -1 -1 : ++ 3 3 -1 0 0

` + 10 -2 1 1 0 0 0 : ++ 2 -2 0 -1 1`

vbox put(6.5,-52)line(0,1)8.5 put(56.25,-69)line(0,1)9 put(56.25,-52)line(0,1)8.5

; ; @ @ @ @ @ ; ; @ @ @10 4 4 5 5 2 4 4 A A A BD AD A A A A A A AD BD A A A fus ind 2D 8A B* 10A B* fus ind 4C 8C D**

: ++ 1 1 1 1 1 : ++ 1 1 1

. + 0 0 0 0 0 . + 0 0 0

. .

: ++ 2 0 0 b5 * . + 0 0 0

: ++ 2 0 0 * b5 .

: ++ -1 1 1 -1 -1 : ++ 1 -1 -1

: ++ 0 r2 -r2 0 0 : oo 0 i2 -i2

First we construct a table whose classes are those of the three
subgroups. Note that the exponent of *A_6* is 60, so the representative
orders could become at most 60 times the value in *2^2*.

gap> s1:= CharTable( "A6.2_1" );; gap> s2:= CharTable( "A6.2_2" );; gap> s3:= CharTable( "A6.2_3" );; gap> c2:= CharTable( "Cyclic", 2 );; gap> v4:= CharTableDirectProduct( c2, c2 );; #I CharTableDirectProduct: existing subgroup fusion on <tbl2> replaced #I by actual one gap> for tbl in [ s1, s2, s3 ] do > Print( tbl.irreducibles[2], "\n" ); > od; [ 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1 ] [ 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1 ] [ 1, 1, 1, 1, 1, -1, -1, -1 ] gap> split:= CharTableSplitClasses( v4, > [1,1,1,1,1,2,2,2,2,2,3,3,3,3,3,4,4,4], 60 );; gap> PrintCharTable( split ); rec( identifier := "Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, \ 3, 3, 3, 4, 4, 4 ])", size := 4, order := 4, name := "Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3,\ 4, 4, 4 ])", centralizers := [ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 ], classes := [ 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/5, 1/3, 1/3, 1/3 ], orders := [ 1, [ 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60 ], [ 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60 ], [ 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60 ], [ 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ], [ 2, 4, 6, 8, 10, 12, 20, 24, 30, 40, 60, 120 ] ], powermap := [ , [ 1, [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ], [ 1, 2, 3, 4, 5 ] ] ], irreducibles := [ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, 1, 1, 1, 1, 1, -1, -1, -1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1 ] ], fusions := [ rec( name := [ 'C', '2', 'x', 'C', '2' ], map := [ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4 ] ) ], operations := CharTableOps )

Now we embed the subgroups and adjust the classlengths, order, centralizers, powermaps and thus the representative orders.

gap> StoreFusion( s1, split, [1,2,3,3,4,5,6,7,8,9,10]); gap> StoreFusion( s2, split, [1,2,3,4,5,5,11,12,13,14,15]); gap> StoreFusion( s3, split, [1,2,3,4,5,16,17,18]); gap> for tbl in [ s1, s2, s3 ] do > fus:= GetFusionMap( tbl, split ); > for class in Difference( [ 1 .. Length( tbl.classes ) ], > KernelChar(tbl.irreducibles[2]) ) do > split.classes[ fus[ class ] ]:= tbl.classes[ class ]; > od; > od; gap> for class in [ 1 .. 5 ] do > split.classes[ class ]:= s3.classes[ class ]; > od; gap> split.classes; [ 1, 45, 80, 90, 144, 15, 15, 90, 120, 120, 36, 90, 90, 72, 72, 180, 90, 90 ] gap> split.size:= Sum( last ); 1440 gap> split.order:= last; gap> split.centralizers:= List( split.classes, x -> split.order / x ); [ 1440, 32, 18, 16, 10, 96, 96, 16, 12, 12, 40, 16, 16, 20, 20, 8, 16, 16 ] gap> split.powermap[3]:= InitPowermap( split, 3 );; gap> split.powermap[5]:= InitPowermap( split, 5 );; gap> for tbl in [ s1, s2, s3 ] do > fus:= GetFusionMap( tbl, split ); > for p in [ 2, 3, 5 ] do > TransferDiagram( tbl.powermap[p], fus, split.powermap[p] ); > od; > od; gap> split.powermap; [ , [ 1, 1, 3, 2, 5, 1, 1, 2, 3, 3, 1, 4, 4, 5, 5, 2, 4, 4 ], [ 1, 2, 1, 4, 5, 6, 7, 8, 6, 7, 11, 13, 12, 15, 14, 16, 17, 18 ],, [ 1, 2, 3, 4, 1, 6, 7, 8, 9, 10, 11, 13, 12, 11, 11, 16, 18, 17 ] ] gap> split.orders:= ElementOrdersPowermap( split.powermap ); [ 1, 2, 3, 4, 5, 2, 2, 4, 6, 6, 2, 8, 8, 10, 10, 4, 8, 8 ]

In order to decide which classes fuse in *G*, we look at the norms of
suitable induced characters, first the + extension of *chi_2* to
*A_6.2_1*.

gap> ind:= Induced( s1, split, [ s1.irreducibles[3] ] )[1]; [ 10, 2, 1, -2, 0, 6, -2, 2, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0 ] gap> ScalarProduct( split, ind, ind ); 3/2

The inertia group of this character is *A_6.2_1*, thus the norm of the
induced character must be 1. If the classes `2B`

and `2C`

fuse, the
contribution of these classes is changed from *15cdot 6^2+15cdot(-2)^2*
to *30 cdot 2^2*, the difference is 480. But we have to subtract 720
which is half the group order, so also `6A`

and `6B`

fuse. This is not
surprising, since it reflects the action of the famous outer automorphism
of *S_6*. Next we examine the + extension of *chi_4* to *A_6.2_2*.

gap> ind:= Induced( s2, split, [ s2.irreducibles[4] ] )[1]; [ 16, 0, -2, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 2*E(5)+2*E(5)^4, 2*E(5)^2+2*E(5)^3, 0, 0, 0 ] gap> ScalarProduct( split, ind, ind ); 3/2

Again, the norm must be 1, `10A`

and `10B`

fuse.

gap> collaps:= CharTableCollapsedClasses( split, > [1,2,3,4,5,6,6,7,8,8,9,10,11,12,12,13,14,15] );; gap> PrintCharTable( collaps ); rec( identifier := "Collapsed(Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2,\ 2, 3, 3, 3, 3, 3, 4, 4, 4 ]),[ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 9, 10, 1\ 1, 12, 12, 13, 14, 15 ])", size := 1440, order := 1440, name := "Collapsed(Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3\ , 3, 3, 3, 3, 4, 4, 4 ]),[ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 9, 10, 11, 12\ , 12, 13, 14, 15 ])", centralizers := [ 1440, 32, 18, 16, 10, 48, 16, 6, 40, 16, 16, 10, 8, 16, 16 ], orders := [ 1, 2, 3, 4, 5, 2, 4, 6, 2, 8, 8, 10, 4, 8, 8 ], powermap := [ , [ 1, 1, 3, 2, 5, 1, 2, 3, 1, 4, 4, 5, 2, 4, 4 ], [ 1, 2, 1, 4, 5, 6, 7, 6, 9, 11, 10, 12, 13, 14, 15 ],, [ 1, 2, 3, 4, 1, 6, 7, 8, 9, 11, 10, 9, 13, 15, 14 ] ], fusionsource := [ "Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4 \ ])" ], irreducibles := [ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, 1, 1, 1, 1, -1, -1, -1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1 ] ], classes := [ 1, 45, 80, 90, 144, 30, 90, 240, 36, 90, 90, 144, 180, 90, 90 ], operations := CharTableOps ) gap> split.fusions; [ rec( name := [ 'C', '2', 'x', 'C', '2' ], map := [ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4 ] ), rec( name := "Collapsed(Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3,\ 3, 3, 4, 4, 4 ]),[ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 9, 10, 11, 12, 12, 1\ 3, 14, 15 ])", map := [ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15 ] ) ] gap> for tbl in [ s1, s2, s3 ] do > StoreFusion( tbl, collaps, > CompositionMaps( GetFusionMap( split, collaps ), > GetFusionMap( tbl, split ) ) ); > od; gap> ind:= Induced( s1, collaps, [ s1.irreducibles[10] ] )[1]; [ 20, -4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] gap> ScalarProduct( collaps, ind, ind ); 1

This character must be equal to any induced character of an irreducible
character of degree 10 of *A_6.2_2* and *A_6.2_3*. That means, `8A`

fuses with `8B`

, and `8C`

with `8D`

.

gap> a6v4:= CharTableCollapsedClasses( collaps, > [1,2,3,4,5,6,7,8,9,10,10,11,12,13,13] );; gap> PrintCharTable( a6v4 ); rec( identifier := "Collapsed(Collapsed(Split(C2xC2,[ 1, 1, 1, 1, 1, 2\ , 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4 ]),[ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8\ , 9, 10, 11, 12, 12, 13, 14, 15 ]),[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10\ , 11, 12, 13, 13 ])", size := 1440, order := 1440, name := "Collapsed(Collapsed(Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, \ 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4 ]),[ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 9, \ 10, 11, 12, 12, 13, 14, 15 ]),[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 11,\ 12, 13, 13 ])", centralizers := [ 1440, 32, 18, 16, 10, 48, 16, 6, 40, 8, 10, 8, 8 ], orders := [ 1, 2, 3, 4, 5, 2, 4, 6, 2, 8, 10, 4, 8 ], powermap := [ , [ 1, 1, 3, 2, 5, 1, 2, 3, 1, 4, 5, 2, 4 ], [ 1, 2, 1, 4, 5, 6, 7, 6, 9, 10, 11, 12, 13 ],, [ 1, 2, 3, 4, 1, 6, 7, 8, 9, 10, 9, 12, 13 ] ], fusionsource := [ "Collapsed(Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3\ , 4, 4, 4 ]),[ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 9, 10, 11, 12, 12, 13, 14\ , 15 ])" ], irreducibles := [ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 1, 1 ] ], classes := [ 1, 45, 80, 90, 144, 30, 90, 240, 36, 180, 144, 180, 180 ], operations := CharTableOps ) gap> for tbl in [ s1, s2, s3 ] do > StoreFusion( tbl, a6v4, > CompositionMaps( GetFusionMap( collaps, a6v4 ), > GetFusionMap( tbl, collaps ) ) ); > od;

Now the classes of *G* are known, the only remaining work is to compute
the irreducibles.

gap> a6v4.irreducibles; [ [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 1, 1 ] ] gap> for tbl in [ s1, s2, s3 ] do > ind:= Set( Induced( tbl, a6v4, tbl.irreducibles ) ); > Append( a6v4.irreducibles, > Filtered( ind, x -> ScalarProduct( a6v4,x,x ) = 1 ) ); > od; gap> a6v4.irreducibles:= Set( a6v4.irreducibles ); [ [ 1, 1, 1, 1, 1, -1, -1, -1, -1, -1, -1, 1, 1 ], [ 1, 1, 1, 1, 1, -1, -1, -1, 1, 1, 1, -1, -1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1, -1 ], [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ], [ 10, 2, 1, -2, 0, -2, -2, 1, 0, 0, 0, 0, 0 ], [ 10, 2, 1, -2, 0, 2, 2, -1, 0, 0, 0, 0, 0 ], [ 16, 0, -2, 0, 1, 0, 0, 0, -4, 0, 1, 0, 0 ], [ 16, 0, -2, 0, 1, 0, 0, 0, 4, 0, -1, 0, 0 ], [ 20, -4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] ] gap> sym:= Symmetrizations( a6v4, [ a6v4.irreducibles[5] ], 2 ); [ [ 45, -3, 0, 1, 0, -3, 1, 0, -5, 1, 0, -1, 1 ], [ 55, 7, 1, 3, 0, 7, 3, 1, 5, -1, 0, 1, -1 ] ] gap> Reduced( a6v4, a6v4.irreducibles, sym ); rec( remainders := [ [ 27, 3, 0, 3, -3, 3, -1, 0, 1, -1, 1, 1, -1 ] ], irreducibles := [ [ 9, 1, 0, 1, -1, -3, 1, 0, -1, 1, -1, -1, 1 ] ] ) gap> Append( a6v4.irreducibles, > Tensored( last.irreducibles, > Sublist( a6v4.irreducibles, [ 1 .. 4 ] ) ) ); gap> SortCharactersCharTable( a6v4, > (1,4)(2,3)(5,6)(7,8)(9,13,10,11,12) );; gap> a6v4.identifier:= "A6.2^2";; gap> DisplayCharTable( a6v4 ); Collapsed(Collapsed(Split(C2xC2,[ 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, \ 3, 3, 3, 4, 4, 4 ]),[ 1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 9, 10, 11, 12, 12,\ 13, 14, 15 ]),[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 13 ])2 5 5 1 4 1 4 4 1 3 3 1 3 3 3 2 . 2 . . 1 . 1 . . . . . 5 1 . . . 1 . . . 1 . 1 . .

1a 2a 3a 4a 5a 2b 4b 6a 2c 8a 10a 4c 8b 2P 1a 1a 3a 2a 5a 1a 2a 3a 1a 4a 5a 2a 4a 3P 1a 2a 1a 4a 5a 2b 4b 2b 2c 8a 10a 4c 8b 5P 1a 2a 3a 4a 1a 2b 4b 6a 2c 8a 2c 4c 8b

X.1 1 1 1 1 1 1 1 1 1 1 1 1 1 X.2 1 1 1 1 1 1 1 1 -1 -1 -1 -1 -1 X.3 1 1 1 1 1 -1 -1 -1 1 1 1 -1 -1 X.4 1 1 1 1 1 -1 -1 -1 -1 -1 -1 1 1 X.5 10 2 1 -2 . 2 2 -1 . . . . . X.6 10 2 1 -2 . -2 -2 1 . . . . . X.7 16 . -2 . 1 . . . 4 . -1 . . X.8 16 . -2 . 1 . . . -4 . 1 . . X.9 9 1 . 1 -1 -3 1 . 1 -1 1 1 -1 X.10 9 1 . 1 -1 -3 1 . -1 1 -1 -1 1 X.11 9 1 . 1 -1 3 -1 . 1 -1 1 -1 1 X.12 9 1 . 1 -1 3 -1 . -1 1 -1 1 -1 X.13 20 -4 2 . . . . . . . . . .

When you start **GAP** it already knows several groups. For example, some
basic groups such as cyclic groups or symmetric groups, all primitive
permutation groups of degree at most 50, and all 2-groups of size at most
256.

Each of the sets above is called a **group library**. The set of all
groups that **GAP** knows initially is called the **collection of group
libraries**.

In this section we show you how you can access the groups in those libraries and how you can extract groups with certain properties from those libraries.

Let us start with the basic groups, because they are not accessed in the same way as the groups in the other libraries.

To access such a basic group you just call a function with an appropriate
name, such as `CyclicGroup`

or `SymmetricGroup`

.

gap> c13 := CyclicGroup( 13 ); Group( ( 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13) ) gap> Size( c13 ); 13 gap> s8 := SymmetricGroup( 8 ); Group( (1,8), (2,8), (3,8), (4,8), (5,8), (6,8), (7,8) ) gap> Size( s8 ); 40320

The functions above also accept an optional first argument that describes
the type of group. For example you can pass `AgWords`

to `CyclicGroup`

Finite Polycyclic Groups).

gap> c13 := CyclicGroup( AgWords, 13 ); Group( c13 )

Of course you cannot pass `AgWords`

to `SymmetricGroup`

, because
symmetric groups are in general not polycyclic.

The default is to construct the groups as permutation groups, but you can
also explicitly pass `Permutations`

. Other possible arguments are
`AgWords`

for finite polycyclic groups, `Words`

for finitely presented
groups, and `Matrices`

for matrix groups (however only `Permutations`

and
`AgWords`

currently work).

Let us now turn to the other libraries. They are all accessed in a uniform way. For a first example we will use the group library of primitive permutation groups.

To extract a group from a group library you generally use the **extraction
function**. In our example this function is called `PrimitiveGroup`

. It
takes two arguments. The first is the degree of the primitive
permutation group that you want and the second is an integer that
specifies which of the primitive permutation groups of that degree you
want.

gap> g := PrimitiveGroup( 12, 3 ); M(11) gap> g.generators; [ ( 2, 6)( 3, 5)( 4, 7)( 9,10), ( 1, 5, 7)( 2, 9, 4)( 3, 8,10), ( 1,11)( 2, 7)( 3, 5)( 4, 6), ( 2, 5)( 3, 6)( 4, 7)(11,12) ] gap> Size( g ); 7920 gap> IsSimple( g ); true gap> h := PrimitiveGroup( 16, 19 ); 2^4.A(7) gap> Size( h ); 40320

The reason for the extraction function is as follows. A group library is usually not stored as a list of groups. Instead a more compact representation for the groups is used. For example the groups in the library of 2-groups are represented by 4 integers. The extraction function hides this representation from you, and allows you to access the group library as if it was a table of groups (two dimensional in the above example).

What arguments the extraction function accepts, and how they are interpreted is described in the sections that describe the individual group libraries in chapter Group Libraries. Those functions will of course signal an error when you pass illegal arguments.

Suppose that you want to get a list of all primitive permutation groups
that have a degree 10 and are simple but not cyclic. It would be very
difficult to use the extraction function to extract all groups in the
group library, and test each of those. It is much simpler to use the
**selection function**. The name of the selection function always begins
with `All`

and ends with `Groups`

, in our example it is thus called
`AllPrimitiveGroups`

.

gap> AllPrimitiveGroups( DegreeOperation, 10, > IsSimple, true, > IsCyclic, false ); [ A(5), PSL(2,9), A(10) ]

`AllPrimitiveGroups`

takes a variable number of argument pairs consisting
of a function (e.g. `DegreeOperation`

) and a value (e.g. 10). To
understand what `AllPrimitiveGroups`

does, imagine that the group library
was stored as a long list of permutation groups. `AllPrimitiveGroups`

takes all those groups in turn. To each group it applies each function
argument and compares the result with the corresponding value argument.
It selects a group if and only if all the function results are equal to
the corresponding value. So in our example `AllPrimitiveGroups`

selects
those groups `g` for which `DegreeOperation(`

`g`) = 10**and**
`IsSimple(`

`g`) = true**and** `IsCyclic(`

. Finally
`g`) = false`AllPrimitiveGroups`

returns the list of the selected groups.

Next suppose that you want all the primitive permutation groups that have
degree **at most** 10, are simple but are not cyclic. You could obtain
such a list with 10 calls to `AllPrimitiveGroups`

(i.e., one call for the
degree 1 groups, another for the degree 2 groups and so on), but there is
a simple way. Instead of specifying a single value that a function must
return you can simply specify a list of such values.

gap> AllPrimitiveGroups( DegreeOperation, [1..10], > IsSimple, true, > IsCyclic, false ); [ A(5), PSL(2,5), A(6), PSL(3,2), A(7), PSL(2,7), A(8), PSL(2,8), A(9), A(5), PSL(2,9), A(10) ]

Note that the list that you get contains `A(5)`

twice, first in its
primitive presentation on 5 points and second in its primitive
presentation on 10 points.

Thus giving several argument pairs to the selection function allows you
to express the logical **and** of properties that a group must have to be
selected, and giving a list of values allows you to express a
(restricted) logical **or** of properties that a group must have to be
selected.

There is no restriction on the functions that you can use. It is even possible to use functions that you have written yourself. Of course, the functions must be unary, i.e., accept only one argument, and must be able to deal with the groups.

gap> NumberConjugacyClasses := function ( g ) > return Length( ConjugacyClasses( g ) ); > end; function ( g ) ... end gap> AllPrimitiveGroups( DegreeOperation, [1..10], > IsSimple, true, > IsCyclic, false, > NumberConjugacyClasses, 9 ); [ A(7), PSL(2,8) ]

Note that in some cases a selection function will issue a warning. For
example if you call `AllPrimitiveGroups`

without specifying the degree,
it will issue such a warning.

gap> AllPrimitiveGroups( Size, [100..400], > IsSimple, true, > IsCyclic, false ); #W AllPrimitiveGroups: degree automatically restricted to [1..50] [ A(6), PSL(3,2), PSL(2,7), PSL(2,9), A(6) ]

If selection functions would really run over the list of all groups in a group library and apply the function arguments to each of those, they would be very inefficient. For example the 2-groups library contains 58760 groups. Simply creating all those groups would take a very long time.

Instead selection functions recognize certain functions and handle them
more efficiently. For example `AllPrimitiveGroups`

recognizes
`DegreeOperation`

. If you pass `DegreeOperation`

to `AllPrimitiveGroups`

it does not create a group to apply `DegreeOperation`

to it. Instead it
simply consults an index and quickly eliminates all groups that have a
different degree. Other functions recognized by `AllPrimitiveGroups`

are
`IsSimple`

, `Size`

, and `Transitivity`

.

So in our examples `AllPrimitiveGroups`

, recognizing `DegreeOperation`

and `IsSimple`

, eliminates all but 16 groups. Then it creates those 16
groups and applies `IsCyclic`

to them. This eliminates 4 more groups
(`C(2)`

, `C(3)`

, `C(5)`

, and `C(7)`

). Then in our last example it
applies `NumberConjugacyClasses`

to the remaining 12 groups and
eliminates all but `A(7)`

and `PSL(2,8)`

.

The catch is that the selection functions will take a large amount of
time if they cannot recognize any special functions. For example the
following selection will take a large amount of time, because only
`IsSimple`

is recognized, and there are 116 simple groups in the
primitive groups library.

` AllPrimitiveGroups( IsSimple, true, NumberConjugacyClasses, 9 );`

So you should specify a sufficiently large set of recognizable functions when you call a selection function. It is also advisable to put those functions first (though in some group libraries the selection function will automatically rearrange the argument pairs so that the recognized functions come first). The sections describing the individual group libraries in chapter Group Libraries tell you which functions are recognized by the selection function of that group library.

There is another function, called the **example function** that behaves
similar to the selection function. Instead of returning a list of all
groups with a certain set of properties it only returns one such group.
The name of the example function is obtained by replacing `All`

by `One`

and stripping the `s`

at the end of the name of the selection function.

gap> OnePrimitiveGroup( DegreeOperation, [1..10], > IsSimple, true, > IsCyclic, false, > NumberConjugacyClasses, 9 ); A(7)

The example function works just like the selection function. That means that all the above comments about the special functions that are recognized also apply to the example function.

Let us now look at the 2-groups library. It is accessed in the same way
as the primitive groups library. There is an extraction function
`TwoGroup`

, a selection function `AllTwoGroups`

, and an example function
`OneTwoGroup`

.

gap> g := TwoGroup( 128, 5 ); Group( a1, a2, a3, a4, a5, a6, a7 ) gap> Size( g ); 128 gap> NumberConjugacyClasses( g ); 80

The groups are all displayed as `Group( a1, a2, ..., a`

, where `n` )*2^n*
is the size of the group.

gap> AllTwoGroups( Size, 256, > Rank, 3, > pClass, 2 ); [ Group( a1, a2, a3, a4, a5, a6, a7, a8 ), Group( a1, a2, a3, a4, a5, a6, a7, a8 ), Group( a1, a2, a3, a4, a5, a6, a7, a8 ), Group( a1, a2, a3, a4, a5, a6, a7, a8 ) ] gap> l := AllTwoGroups( Size, 256, > Rank, 3, > pClass, 5, > g -> Length( DerivedSeries( g ) ), 4 );; gap> Length( l ); 28

The selection and example function of the 2-groups library recognize
`Size`

, `Rank`

, and `pClass`

. Note that `Rank`

and `pClass`

are
functions that can in fact only be used in this context, i.e., they can
not be applied to arbitrary groups.

The following discussion is a bit technical and you can ignore it safely.

For very big group libraries, such as the 2-groups library, the groups (or their compact representations) are not stored on a single file. This is because this file would be very large and loading it would take a long time and a lot of main memory.

Instead the groups are stored on a small number of files (27 in the case of the 2-groups). The selection and example functions are careful to load only those files that may actually contain groups with the specified properties. For example in the above example the files containing the groups of size less than 256 are never loaded. In fact in the above example only one very small file is loaded.

When a file is loaded the selection and example functions also unload the previously loaded file. That means that they forget all the groups in this file again (except those selected of course). Thus even if the selection or example functions have to search through the whole group library, only a small part of the library is held in main memory at any time. In principle it should be possible to search the whole 2-groups library with as little as 2 MByte of main memory.

If you have sufficient main memory available you can explicitly load
files from the 2-groups library with `ReadTwo( `

, e.g.,
`filename` )`Read( "twogp64" )`

to load the file with the groups of size 64. Those
files will then not be unloaded again. This will take up more main
memory, but the selection and example function will work faster, because
they do not have to load those files again each time they are needed.

In this section you have seen the basic groups library and the group libraries of primitive groups and 2-groups. You have seen how you can extract a single group from such a library with the extraction function. You have seen how you can select groups with certain properties with the selection and example function. Chapter Group Libraries tells you which other group libraries are available.

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 `is`

with the value `Cat``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

, the
commutator subgroup of a group is stored in the knowledge component
`D`.size

, 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.
`D`.commutatorSubgroup

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

, its value is a record. For each function that you can
apply to `D`.operations`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

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
`D`.operations.`function` := `new-function`;`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

and
only afterwards do `D`.operations := Copy( `D`.operations );

.
`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.

- 1:
- How can we represent domains in such a way that we can handle domains of different type in a common way?

- 2:
- How can we make it possible to allow functions that take domains of different type and perform the same operation for those domains (but using different methods)?

- 3:
- How can we make it possible that the implementation of a new type of domains only requires that one implements what distinguishes this new type of domains from domains of an old type (without the need to change any old code)?

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.

- 1:
- How can we represent objects in such a way that we can handle objects of different classes in a common way (e.g., declare variables that can hold objects of different classes)?

- 2:
- How can we make it possible to allow functions that take objects of different classes (with a common base class) and perform the same operation for those objects (but using different methods)?

- 3:
- How can we make it possible that the implementation of a new class of objects only requires that one implements what distinguishes the objects of this new class from the objects of an old (base) class (without the need to change any old code)?

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).

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

,
where `a` + `b`*E(4)`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(`

tests whether `x`)`x` is a cyclotomic integer and
`NofCyc(`

returns the smallest `x`)`n` such that the cyclotomic `x` can
be written as a linear combination of powers of the primitive `n`-th root
of unity `E(`

. If `n`)`NofCyc(`

returns 1, `x`)`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

as a
point in the complex plane and returns the point of the lattice spanned
by 1 and `x`/`y``E(4)`

**closest** to the point

. If we would truncate
towards the origin instead (this is done by the function `x`/`y``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.

In this section we will show you an example that is slightly more complex than the example in the previous section. Namely we will demonstrate how one can implement parametrized domains. As an example we will implement symmetric permutation groups. This works similar to the implementation of a single domain. Therefore we can be very brief. Of course you should have read the previous section.

Note that everything defined here is already in the file
`GRPNAME/"permgrp.grp"`

, 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.

In the example of the previous section we simply had a variable
(`GaussianIntegers`

), whose value was the domain. This can not work in
this example, because there is not **one** symmetric permutation group.
The solution is obvious. We simply define a function that takes the
degree and returns the symmetric permutation group of this degree (as a
domain).

gap> SymmetricPermGroup := function ( n ) > local G; # symmetric group on <n> points, result > > # make the group generated by (1,n), (2,n), .., (n-1,n) > G := Group( List( [1..n-1], i -> (i,n) ), () ); > G.degree := n; > > # give it the correct operations record > G.operations := SymmetricPermGroupOps; > > # return the symmetric group > return G; > end;;

The key is of course to give the domains returned by `SymmetricPermGroup`

a new operations record. This operations record will hold functions that
are written especially for symmetric permutation groups. Note that all
symmetric groups created by `SymmetricPermGroup`

share one operations
record.

Just as we inherited in the example in the previous section from the
operations record `RingOps`

, here we can inherit from the operations
record `PermGroupOps`

(after all, each symmetric permutation group is
also a permutation group).

` gap> SymmetricPermGroupOps := Copy( PermGroupOps ); `

We will now overlay some of the functions in this operations record with new functions that make use of the fact that the domain is a full symmetric permutation group. The first function that does this is the membership test function.

gap> SymmetricPermGroupOps.\in := function ( g, G ) > return IsPerm( g ) > and ( g = () > or LargestMovedPointPerm( g ) <= G.degree); > end;;

The most important knowledge for a permutation group is a base and a strong generating set with respect to that base. It is not important that you understand at this point what this is mathematically. The important point here is that such a strong generating set with respect to an appropriate base is used by many generic permutation group functions, most of which we inherit for symmetric permutation groups. Therefore it is important that we are able to compute a strong generating set as fast as possible. Luckily it is possible to simply write down such a strong generating set for a full symmetric group. This is done by the following function.

gap> SymmetricPermGroupOps.MakeStabChain := function ( G, base ) > local sgs, # strong generating system of G wrt. base > last; # last point of the base > > # remove all unwanted points from the base > base := Filtered( base, i -> i <= G.degree ); > > # extend the base with those points not already in the base > base := Concatenation( base, Difference( [1..G.degree], base ) ); > > # take the last point > last := base[ Length(base) ]; > > # make the strong generating set > sgs := List( [1..Length(base)-1], i -> ( base[i], last ) ); > > # make the stabilizer chain > MakeStabChainStrongGenerators( G, base, sgs ); > end;;

One of the things that are very easy for symmetric groups is the
computation of centralizers of elements. The next function does this.
Again it is not important that you understand this mathematically. The
centralizer of an element `g` in the symmetric group is generated by the
cycles `c` of `g` and an element `x` for each pair of cycles of `g` of
the same length that maps one cycle to the other.

gap> SymmetricPermGroupOps.Centralizer := function ( G, g ) > local C, # centralizer of g in G, result > sgs, # strong generating set of C > gen, # one generator in sgs > cycles, # cycles of g > cycle, # one cycle from cycles > lasts, # lasts[l] is the last cycle of length l > last, # one cycle from lasts > i; # loop variable > > # handle special case > if IsPerm( g ) and g in G then > > # start with the empty strong generating system > sgs := []; > > # compute the cycles and find for each length the last one > cycles := Cycles( g, [1..G.degree] ); > lasts := []; > for cycle in cycles do > lasts[Length(cycle)] := cycle; > od; > > # loop over the cycles > for cycle in cycles do > > # add that cycle itself to the strong generators > if Length( cycle ) <> 1 then > gen := [1..G.degree]; > for i in [1..Length(cycle)-1] do > gen[cycle[i]] := cycle[i+1]; > od; > gen[cycle[Length(cycle)]] := cycle[1]; > gen := PermList( gen ); > Add( sgs, gen ); > fi; > > # and it can be mapped to the last cycle of this length > if cycle <> lasts[ Length(cycle) ] then > last := lasts[ Length(cycle) ]; > gen := [1..G.degree]; > for i in [1..Length(cycle)] do > gen[cycle[i]] := last[i]; > gen[last[i]] := cycle[i]; > od; > gen := PermList( gen ); > Add( sgs, gen ); > fi; > > od; > > # make the centralizer > C := Subgroup( G, sgs ); > > # make the stabilizer chain > MakeStabChainStrongGenerators( C, [1..G.degree], sgs ); > > # delegate general case > else > C := PermGroupOps.Centralizer( G, g ); > fi; > > # return the centralizer > return C; > end;;

Note that the definition `C := Subgroup( G, sgs );`

defines a subgroup
of a symmetric permutation group. But this subgroup is usually not a
full symmetric permutation group itself. Thus `C`

must not have the
operations record `SymmetricPermGroupOps`

, instead it should have the
operations record `PermGroupOps`

. And indeed `C`

will have this
operations record. This is because `Subgroup`

calls

, and we inherited this function from
`G`.operations.Subgroup`PermGroupOps`

.

Note also that we only handle one special case in the function above.
Namely the computation of a centralizer of a single element. This
function can also be called to compute the centralizer of a whole
subgroup. In this case `SymmetricPermGroupOps.Centralizer`

simply
**delegates** the problem by calling `PermGroupOps.Centralizer`

.

The next function computes the conjugacy classes of elements in a symmetric group. This is very easy, because two elements are conjugated in a symmetric group when they have the same cycle structure. Thus we can simply compute the partitions of the degree, and for each degree we get one conjugacy class.

gap> SymmetricPermGroupOps.ConjugacyClasses := function ( G ) > local classes, # conjugacy classes of G, result > prt, # partition of G > sum, # partial sum of the entries in prt > rep, # representative of a conjugacy class of G > i; # loop variable > > # loop over the partitions > classes := []; > for prt in Partitions( G.degree ) do > > # compute the representative of the conjugacy class > rep := [2..G.degree]; > sum := 1; > for i in prt do > rep[sum+i-1] := sum; > sum := sum + i; > od; > rep := PermList( rep ); > > # add the new class to the list of classes > Add( classes, ConjugacyClass( G, rep ) ); > > od; > > # return the classes > return classes; > end;;

This concludes this example. You have seen that the implementation of a parametrized domain is not much more difficult than the implementation of a single domain. You have also seen how functions that overlay generic functions may delegate problems back to the generic function. The library file for symmetric permutation groups contain some more functions for symmetric permutation groups.

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

should be equivalent to `g`/`h`

. So the
function for `g`*`h`^-1`/`

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

is `l` < `r``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

, this function is not called. This is
because the multiplication function in the operations record of the
`subgroup` * `residue-class`**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( `

. 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.
`representative`, `modulus` )

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( [ `

(see
Domain). `res1`, `res2`... ] )`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

is `res1` in GroupElements`true`

, because `GroupElements`

accepts all
records with tag `isGroupElement`

. So `Domain`

returns `GroupElements`

.
`Group`

then calls

`GroupElements.operations.Group(GroupElements,[`

,
where `res1`,`res2`...],`id`)`id` is the identity residue class, obtained by

, and
returns the result.
`res1` ^ 0

`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`

.

will be the list of prime power factors of `G`.facts

.
Actually this is a little bit more complicated, because the residue class
group modulo the largest power `G`.modulus`q` of 2 that divides

need
not be cyclic. So if `G`.modulus`q` is a multiple of 4,

will be 4,
corresponding to the projection of `G`.facts[1]`G` into *(Z / 4 Z)^** (of size 2),
furthermore if `q` is a multiple of 8,

will be `G`.facts[2]`q`,
corresponding to the projection of `G` into the subgroup generated by 5
in *(Z / q Z)^** (of size *q/4*).

will be a list of primitive roots, i.e., of generators of the
corresponding factors in `G`.roots

. `G`.facts

will be a list of the
sizes of the corresponding factors in `G`.sizes

, i.e., `G`.facts

. (If `G`.sizes[`i`]
= Phi( `G`.facts[`i`] )

is a multiple of 8,
`G`.modulus

will be 5, and `G`.roots[2]

will be `G`.sizes[2]`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

, where
`G`.facts

is the logarithm of `e`[`i`]

with respect to `g`.representative mod `G`.facts[`i`]

. The multiplication of elements of `G`.roots[`i`]`G`
corresponds to the componentwise addition of their exponent vectors,
where we add modulo

in the `G`.sizes[`i`]`i`-th component. (Again
special consideration are necessary if

is divisible by 8.)
`G`.modulus

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

. This new matrix obviously still
contains the exponent vectors of a generating system of `G`.sgs`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

, and multiply the orders of the leading exponents of the
nonzero exponent vectors.
`G`.sgs

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

, 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.)
`G`.sgs

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

April 1997