The Freya Programming Language

Generic types

See also

Generic types defer the specification of one or more types in a type declaration by using type parameters in the type definition.

Generic type parameters

Class declarations may include type parameters, as the X in this example:

Stack = class[X]
private
    Items: Array[X];
    Count: Integer;
public
    constructor;
    method Push(Value: X);
    method Pop: X;

    property IsEmpty: Boolean;
    property Top: X;
end;

Before using the Stack class, you must supply real types for the generic parameter X:

var myStack: Stack[Integer] := new Stack[Integer];

When you associate a real type to the X parameter, the .NET runtime creates a new class. The new class has an Items field, which is an array of integers, and a Top property returning an integer value.

Generic constraints

The allowed operations on a variable or field declared with a type parameter are very limited. The reason, of course, is that you don't know, in advance, which actual type will be supplied for the type parameter. You can safely assume the actual type will be a System.Object heir, so you can safely call the Equals method, for instance.

Most of the times, you'll need more specialized operations. If you're writing a generic sorted list, you need to compare two different values to find which is the greatest. This means, in the first place, that you won't use types without support for comparison for instantiating your generic sorted list. The accepted way to specify this behavior is using generic constraints when declaring generic type parameters:

SortedList = class[X(IComparable)]
    // ...
end;

Now, if you have two variables declared with X, you can use the CompareTo method from the IComparable interface:

var v1, v2: X;
// ...
if v1.CompareTo(v2) < 0 then
    Console.WriteLine('First value lesser than the second value');

Actually, we have asked for the non-generic IComparable interface, which CompareTo method takes an Object parameter. We can do a better job by asking for the corresponding generic interface:

SortedList = class[X(IComparable[X])]
    // ...
end;

Though it looks like a recursive constraint, it isn't. In order to understand why there's no recursivity here at all, we must trace how the SortedList[X] generic type bounds its generic argument X to a concrete data type. Let's say we want a sorted list of strings:

  1. So, what we want is a SortedList[String].
  2. Now, we have identified the X in the open type with String, in our closed type.
  3. Only then, the compiler asks: does this binding satisfy all the constraint? The constraint for X states that X must implement the interface IComparable[X].
  4. But as we now, X has been substituted by the String type. Our constraint actually says that String must implement IComparable[String]. Is this true? Of course it is...

Did you see any recursion in action?

Constraint kinds

Let's summarize which constraint kinds can be applied to a generic type parameter:

This constraint verifies that the type argument derives from a given base class. You cannot specify more than a base class for a generic parameter, for obvious reasons. However, if you omit a base class constraint, the compiler can designate a base class depending in which other constraints are specified for the parameter. For instance, if you add a record constraint (see below), the base class is automatically changed to System.ValueType.

Base class constraints are included after the generic parameter declaration, surrounded by parenthesis:

ControlList = class[TControl(Control)]
    // ...
end;

Most frequently, you'll require that a given argument type must implement one or more interfaces.

HashTable = class[T(IEquatable[T])]
    // ...
end;

These constraints force the type argument to be a value type or a reference type. Class and record constraints are declared including one of the reserved words class or record as a prefix of the generic parameter type, as shown in the following example:

PolymorphicList = class[class T]
    // ...
end;

The inclusion of a class constraint, for instance, allows you to use the as typecast operator:

    MyClass = class[class X]
    public
        method GetItemAsControl(Item: X): Control;
        // ...
    end;

implementation for MyClass[X] is

    method GetItemAsControl(Item: X): Control;
    begin
        Result := Item as Control;
    end;

If the type parameter X were not constrained to be a reference type, the typecast inside GetItemAsControl would be considered as an error by the compiler.

This constraint requires the type argument to feature a parameterless constructor. Syntactically, you specify this constraint by adding the new keyword to the constraint list associated with the type parameter.

See also

The Freya Programming Language
Constructed types
Array types
Type declarations
Class declarations
Interface declarations
Inheritance
Nullable types
Generic methods