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). The function's parameters may fill the type's in any order: func Take<K, V>(entry: Entry<V, K>) -> K, given an Entry<int32, bool>, infers K = bool and V = int32.

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. A method declared there belongs to Pair<int32> only; with func Sum(self: &Pair<int32>) -> int32 in that block, calling Sum on a Pair<int64> is refused:

error: type 'Pair<int64>' has no method 'Sum'
  note: 'Sum' is declared in 'extend Pair<int32>'
  help: call it on the instantiation its extension names

Extensions of two instantiations, extend Pair<int32> and extend Pair<int64>, may each declare a method of the same name and parameters, since no receiver can call both. A method of the same signature in extend Pair<T> competes with each of them and is refused with function 'Sum' has the same parameter signature as an earlier overload.

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, and converts as any implementing type does: a Box<int32> becomes an interface value of type Named, or a &Named borrow.

See also