User-Defined Types

Since Magma V2.19, types may be defined by users within packages. This facility allows the user to declare new type names and create objects with such types and then supply some basic primitives and intrinsic functions for such objects.

The new types are known as user-defined types. The way these are typically used is that after declaring such a type T, the user supplies package intrinsics to: (1) create objects of type T and set relevant attributes to define the objects; (2) perform some basic primitives which are common to all objects in Magma; (3) perform non-trivial computations on objects of type T.

Contents

Declaring User-Defined Types

The following declarations are used to declare user-defined types. They may only be placed in package files, i.e., files that are included either by using Attach or a spec file (see above). Declarations may appear in any package file and at any place within the file at the top level (not in a function, etc.). In particular, it is not required that the declaration of a type appears before package code which refers to the type (as long as the type is declared before running the code). Examples below will illustrate how the basic declarations are used.

declare type T;
Declare the given type name T (without quotes) to be a user-defined type.
declare type T: P1, ..., Pn;
Declare the given type name T (without quotes) to be a user-defined type, and also declare T to inherit from the user types P1, ..., Pn (which must be declared separately). As a result, ISA(T, Pi) will be true for each i and when intrinsic signatures are scanned at a function call, an object of type T will match an argument of a signature with type Pi for any i.

NB: currently one may not inherit from existing Magma internal types or virtual types (categories). It is hoped that this restriction will be removed in the future.

declare type T[E];
Declare the given type names T and E (both without quotes) to be user-defined types. This form also specifies that E is the element type corresponding to T; i.e., if an object x has an element of type T for its parent, then x must have type E. This relationship is needed for the construction of sets and sequences which have objects of type T as a universe. The type E may also be declared separately, but this is not necessary.
declare type T[E]: P1, ..., Pn;
This is a combination of the previous kinds two declarations: T and E are declared as user-defined types while E is also declared to be the element type of T, and T is declared to inherit from user-defined types P1, ..., Pn.

Creating an Object

New(T) : Type -> T
Create an empty object of type T, where T is a user-defined type. Typically, after setting X to the result of this function, the user should set attributes in X to define relevant properties of the object which are characteristic of objects of type T.

Special Intrinsics Provided by the User

Let T be a user-defined type. Besides the declaration of T, the following special intrinsics are mostly required to be defined for type T (the requirements are specified for each kind of intrinsic). These intrinsics allow the internal Magma functions to perform some fundamental operations on objects of type T. Note that the special intrinsics need not be in one file or in the same file as the declaration.

intrinsic Print(X::T)
{Print X}
    // Code: Print X with no new line, via printf
end intrinsic;
intrinsic Print(X::T, L::MonStgElt)
{Print X at level L}
    // Code: Print X at level L with no new line, via printf
end intrinsic;

Exactly one of these intrinsics must be provided by the user for type T. Each is a procedure rather than a function (i.e., nothing is returned), and should contain one or more print statements. The procedure is called automatically by Magma whenever the object X of type T is to be printed. A new line should not occur at the end of the last (or only) line of printing: one should use printf (see examples below).

When the second form of the intrinsic is provided, it allows X to be printed differently depending on the print level L, which is a string equal to one of "Default", "Minimal", "Maximal", "Magma".

Note: Print is used by Magma in error-handling tracebacks, being called with the "Minimal" parameter if available. In particular, Print in this form should be kept rather uncomplicated, notably avoiding the usage of (failing) try/catch constructions, as these will cause a crash from recursive errors being detected.

intrinsic Parent(X::T) -> .
{Parent of X}
    // Code: Return the parent of X
end intrinsic;

This intrinsic is only needed when T is an element type, so objects of type T have parents. It should be a user-provided package function, which takes an object X of type T (user-defined), and returns the parent of X, assuming it has one. In such a case, typically the attribute Parent will be defined for X and so X`Parent should simply be returned.

intrinsic 'in'(e::., X::T) -> BoolElt
{Return whether e is in X}
    // Code: Return whether e is in X
end intrinsic;

This intrinsic is only needed when objects of type T (user-defined) have elements, and should be a user-provided package function, which takes any object e and an object X of type T (user-defined), and returns whether e is an element of X.

intrinsic IsCoercible(X::T, y::.) -> BoolElt, .
{Return whether y is coercible into X and the result if so}
    // Code: do tests on the type of y to see whether coercible
    // On failure, do:
    //    return false, "Illegal coercion"; // Or more particular message
    // Assumed coercible now; set x to result of coercion into X
    return true, x;
end intrinsic;

Assuming that objects of type T (user-defined) have elements (and so coercion into such objects makes sense), this must be a user-provided package function, which takes an object X of type T (user-defined) and an object Y of any type. If Y is coercible into X, the function should return true and the result of the coercion (whose parent should be X). Otherwise, the function should return false and a string giving the reason for failure. If this package intrinsic is provided, then the coercion operation X!y will also automatically work for an object X of type T (i.e., the internal coercion code in Magma will automatically call this function).

intrinsic SubConstructor(X::T, t::.) -> T
{Return the substructure of X generated by elements of the tuple t}
    // This corresponds to the constructor call sub<X | r1, r2, ..., rn>
    // t is ALWAYS a tuple of the form <r1, r2, ..., rn>
    // Code: do tests on the elements in t to see whether valid and then
    //       set S to the substructure of T generated by r1, r2, ..., rn
    // Use standard require statements for error checking
    // Possibly use "t := Flat(t);" to make it easy to loop over t if
    //     any of the ri are sequences
    return S;
end intrinsic;

Assuming that objects of type T (user-defined) have elements, this must be a user-provided package function, which takes an object X of type T (user-defined) and a tuple t. The user call sub<X | r1, r2, ..., rn> (where X has type T) will cause this intrinsic to be called with X and the tuple t=< r1, ... ,rn >. The function should create the substructure S of X generated by r1, ... ,rn and return S alone (the inclusion map from X to S is automatically handled by Magma via coercion).

intrinsic Hash(X::T) -> RngIntElt
{Return a hash value for the object x (should be between 0 and 2^31-1)}
   // Code: determine a hash value for the given object
   // NOTE: Objects X and Y of type T for which X eq Y is true
   //       MUST have the same hash value
   return hash;
end intrinsic;

Providing this intrinsic can greatly speed the checking of equality of objects of type T, and in particular if you wish to work with sets of reasonable cardinality (more than 1000 elements) it should be made available. The requirement is that if X and Y are equal, then their hashes should be the same, regardless of their internal representation.

Examples

Some basic examples illustrating the general use of user-defined types are given here. Non-trivial examples can also be found in much of the standard Magma package code (one can search for "declare type" in the package .m files to see several typical uses).

Example Func_MyRat (H2E17)

In this first simple example, we create a user-defined type MyRat which is used for a primitive representation of rational numbers. Of course, a serious version would keep the numerators & denominators always reduced, but for simplicity we skip such details. We define the operations + and * here; one would typically add other operations like -, eq and IsZero, etc.
declare type MyRat;
declare attributes MyRat: Numer, Denom;
intrinsic MyRational(n::RngIntElt, d::RngIntElt) -> MyRat
{Create n/d}
    require d ne 0: "Denominator must be non-zero";
    r := New(MyRat);
    r`Numer := n;
    r`Denom := d;
    return r;
end intrinsic;
intrinsic Print(r::MyRat)
{Print r}
    n := r`Numer;
    d := r`Denom;
    g := GCD(n, d);
    if d lt 0 then g := -g; end if;
    printf "%o/%o", n div g, d div g; // NOTE: no newline!
end intrinsic;
intrinsic '+'(r::MyRat, s::MyRat) -> MyRat
{Return r + s}
    rn := r`Numer;
    rd := r`Denom;
    sn := s`Numer;
    sd := s`Denom;
    return MyRational(rn*sd + sn*rd, rd*sd);
end intrinsic;
intrinsic '*'(r::MyRat, s::MyRat) -> MyRat
{Return r * s}
    rn := r`Numer;
    rd := r`Denom;
    sn := s`Numer;
    sd := s`Denom;
    return MyRational(rn*sn, rd*sd);
end intrinsic;
Assuming the above code is placed in a file MyRat.m, one could attach it in Magma and then do some simple operations, as follows.
> Attach("myrat.m");
> r := MyRational(3, -9);
> r;
-1/3
> s := MyRational(4, 7);
> s;
> r+s;
5/21
> r*s;
-4/21

Example Func_UserTypes2 (H2E18)

In this example, we define a type DirProd for direct products of rings, and a corresponding element type DirProdElt for their elements. Objects of type DirProd contain a tuple Rings with the rings making up the direct product, while objects of type DirProdElt contain a tuple Element with the elements of the corresponding rings, and also a reference to the parent direct product object.
/* Declare types and attributes */
// Note that we declare DirProdElt as element type of DirProd:
declare type DirProd[DirProdElt];
declare attributes DirProd: Rings;
declare attributes DirProdElt: Elements, Parent;
/* Special intrinsics for DirProd */
intrinsic DirectProduct(Rings::Tup) -> DirProd
{Create the direct product of given rings (a tuple)}
    require forall{R: R in Rings | ISA(Type(R), Rng)}:
        "Tuple entries are not all rings";
    D := New(DirProd);
    D`Rings := Rings;
    return D;
end intrinsic;
intrinsic Print(D::DirProd)
{Print D}
    Rings := D`Rings;
    printf "Direct product of %o", Rings; // NOTE: no newline!
end intrinsic;
function CreateElement(D, Elements)
    // Create DirProdElt with parent D and given Elements
    x := New(DirProdElt);
    x`Elements := Elements;
    x`Parent := D;
    return x;
end function;
intrinsic IsCoercible(D::DirProd, x::.) -> BoolElt, .
{Return whether x is coercible into D and the result if so}
    Rings := D`Rings;
    n := #Rings;
    if Type(x) eq DirProdElt then
        if x`Parent cmpeq D then
            return true, x;
        end if;
        x := x`Elements;
    end if;
    if Type(x) ne Tup then
        return false, "Coercion RHS must be a tuple";
    end if;
    if #x ne n then
        return false, "Wrong length of tuple for coercion";
    end if;
    Elements := <>;
    for i := 1 to n do
        l, t := IsCoercible(Rings[i], x[i]);
        if not l then
            return false, Sprintf("Tuple entry %o not coercible", i);
        end if;
        Append(~Elements, t);
    end for;
    y := CreateElement(D, Elements);
    return true, y;
end intrinsic;
/* Special intrinsics for DirProdElt */
intrinsic Print(x::DirProdElt)
{Print x}
    printf "%o", x`Elements; // NOTE: no newline!
end intrinsic;
intrinsic Parent(x::DirProdElt) -> DirProd
{Parent of x}
    return x`Parent;
end intrinsic;
intrinsic '+'(x::DirProdElt, y::DirProdElt) -> DirProdElt
{Return x + y}
    D := Parent(x);
    require D cmpeq Parent(y): "Incompatible arguments";
    Ex := x`Elements;
    Ey := y`Elements;
    return CreateElement(D, <Ex[i] + Ey[i]: i in [1 .. #Ex]>);
end intrinsic;
intrinsic '*'(x::DirProdElt, y::DirProdElt) -> DirProdElt
{Return x * y}
    D := Parent(x);
    require D cmpeq Parent(y): "Incompatible arguments";
    Ex := x`Elements;
    Ey := y`Elements;
    return CreateElement(D, <Ex[i] * Ey[i]: i in [1 .. #Ex]>);
end intrinsic;
A sample Magma session using the above package is as follows. We create elements x, y of a direct product D and do simple operations on x, y. One would of course add other intrinsic functions for basic operations on the elements.
> Attach("DirProd.m");
> Z := IntegerRing();
> Q := RationalField();
> F8<a> := GF(2^3);
> F9<b> := GF(3^2);
> D := DirectProduct(<Z, Q, F8, F9>);
> x := D!<1, 2/3, a, b>;
> y := D!<2, 3/4, a+1, b+1>;
> x;
<1, 2/3, a, b>
> Parent(x);
Direct product of <Integer Ring, Rational Field, Finite field of
size 2^3, Finite field of size 3^2>
> y;
<2, 3/4, a^3, b^2>
> x+y;
<3, 17/12, 1, b^3>
> x*y;
<2, 1/2, a^4, b^3>
> D!x;
<1, 2/3, a, b>
> S := [x, y]; S;
[
    <1, 2/3, a, b>,
    <2, 3/4, a^3, b^2>
]
>
> &+S;
<3, 17/12, 1, b^3>
V2.28, 13 July 2023