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>>.
| Written | Type 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::Missing | the 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:
| Written | Error |
|---|---|
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
- Generics — generic functions and inference
- Generic methods — methods of generic types
- Structures and Variants — the non-generic forms
- Learn: Generic type
Overview
Functions, structures and variants take type parameters. Each use is compiled separately, with type arguments written out or inferred from the arguments.
Generic Methods
Methods of a generic type use its parameters, and any method may declare type parameters of its own, inferred or written after the method name.