Generic Types

A structure or a variant may declare type parameters after its name. The declaration is a pattern, not yet a type: Pair<int32> and Pair<char8[..]> are two distinct types stamped out of it, each with its own layout, sized from its own type arguments.

Declaration

struct Name<T, U, …> { fields }
variant Name<T, …> { cases }
struct Name<T: Bound> { fields }
struct Pair<T> {
    first: T;
    second: T;
}

struct Entry<K, V> {
    key: K;
    value: V;
}

variant Reading<T> {
    Exact(T),
    Between(T, T),
    Missing
}

The parameters are in scope in the fields and case payloads. Enums cannot be generic — enum 'Level' cannot declare type parameters, with the help to use a variant — and neither can unions, type aliases or interfaces.

Instantiation

A generic type is always named with its type arguments, filled in order: Pair<int32>, Entry<char8[..], int32>, Reading<float64>. They may themselves be instantiations, and nested argument lists may close on one >> or >>>: Pair<Pair<int32>>.

WrittenType arguments come from
let p: Pair<int32> = …;the annotation
Pair<int32> { first: 3, second: 4 }the literal — always written
Pair<int32>(5)the constructor call — always written
Reading::Exact(7)the payload, unless an annotation gives them
Reading::Missingthe destination: an annotation, a parameter or a return type
Reading::Missing<int32>()the case itself
let point = Pair<int32> { first: 3, second: 4 };
let entry = Entry<char8[..], int32> { key: "age", value: 7 };
let temperature = Reading::Between(18.5, 21.0);   // Reading<float64>
let floor: Reading<int32> = Reading::Exact(7);    // the annotation wins: 7 is an int32
let lost: Reading<int32> = Reading::Missing;
let other = Reading::Missing<int32>();

A structure literal never infers its type arguments from its fields, and a field declared T must then have exactly that type:

WrittenError
Pair { first: 3, second: 4 }struct initializer for 'Pair' requires 1 type argument, but 0 were provided
Entry<int32> { key: 1, value: 2 }struct initializer for 'Entry' requires 2 type arguments, but 1 was provided
Pair(5)constructor for 'Pair' requires 1 type argument, but 0 were provided
let lost = Reading::Missing;variant case 'Reading::Missing' requires 1 type argument, but 0 were provided
Pair<int32> { first: 3, second: "four" }field 'second' in initializer for 'Pair' has type 'char8[..]', but its declaration requires 'int32'

A generic function taking a generic type infers its own parameters from the argument, so the caller of func Swapped<T>(pair: Pair<T>) -> Pair<T> writes Swapped(point).

Layout

Every instantiation is laid out from its own type arguments, so Pair<int64> is twice the size of Pair<int32>, and a Box<Wide> reserves the full width of Wide for its field. Two instantiations are unrelated types: a Pair<int32> is not a Pair<int64>, and neither converts to the other.

Methods and extensions

Methods, constructors, operators, destructors and the copy and move operations of a generic type live in an extend block that names the type with its own parameter names:

extend Pair<T> {
    func Pair(both: T) -> Pair<T> {
        return Pair<T> { first: both, second: both };
    }

    func Swapped(self: &Pair<T>) -> Pair<T> {
        return Pair<T> { first: self.second, second: self.first };
    }
}

One block serves every instantiation. The parameter names must match the declaration's: extend Pair { … } is struct type 'Pair' requires 1 type argument, but 0 were provided, and extend Pair<U> for a type declared Pair<T> is type 'U' is not defined in this scope. See Generic methods for calling them, and for methods with type parameters of their own.

An extend block may also name one instantiation, extend Pair<int32> { … }, to give that instantiation alone extra methods.

Methods of one instantiation are not yet restricted to it.
rux 0.4.0 does not yet check the receiver's type arguments: a method declared in extend Pair<int32> can be called on a Pair<int64>, and reads it as a Pair<int32>. Call such a method only on the instantiation it was written for.

Bounds on type parameters

A type parameter of a structure may carry bounds. Every instantiation is checked against them where it is written, and methods of the type may call the bounds' requirements on its fields:

interface Scored {
    func Score() -> int32;
}

struct Board<T: Scored> {
    leader: T;
}

extend Board<T> {
    func Top(self: &Board<T>) -> int32 {
        return self.leader.Score();
    }
}
error: type argument 'float64' does not satisfy interface bound 'Scored' on type parameter 'T'
  note: interface 'Scored' requires method 'Score', which type 'float64' does not implement
  note: type parameter 'T' of struct 'Board' is bound by 'Scored'
  help: implement the interface, as in 'extend float64: Scored { ... }'

Implementing interfaces

A generic type implements an interface for every instantiation at once, with extend Box<T> : Named { … }. Each instantiation then satisfies a Named bound.

Generic instantiations do not yet convert to interface values.
rux 0.4.0 does not yet turn a Box<int32> into an interface value of type Named, or into a &Named borrow, even when Box<T> implements Named; the conversion is refused as a type mismatch. Bounds work.
Swapped type parameters.
A function whose parameter names a generic type with its arguments swapped, such as func Take<K, V>(entry: Entry<V, K>), makes rux 0.4.0 crash during rux check. Give the function's parameters names that differ from the type's, as in func Take<A, B>(entry: Entry<B, A>).

See also