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).
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.
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.
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.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
- 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.