Generics¶
A type parameter lets one declaration work for several types without giving up the type check. Write it in angle brackets after the name, then use it wherever a type goes.
struct Cell<T> {
value: T,
}
impl Cell<T> {
fn get(self) -> T {
return self.value;
}
}
fn main() {
let c = Cell<int>{ value: 3 };
print(c.get());
}
Cell<int> and Cell<string> are separate types. A value of one is not
accepted where the other is expected, and each gets its own copy of the methods,
so a method body sees the concrete type it was compiled for.
Generic functions¶
A function declares its type parameters after its name and may use them in the parameter annotations and in the return annotation.
fn first<T>(items: T[]) -> T {
return items[0];
}
fn main() {
let nums = [10, 20, 30];
print(first(nums));
print(first<int>(nums));
}
Both calls work. Leaving the type argument off falls back to the inference every
un-annotated parameter gets; naming it pins the parameters written in terms of
it, so first<int> rejects a float[].
Name the type argument when the type does not appear in the arguments at all:
Signal<T> never stores a T. An unused type parameter is legal, and here it
is the whole point: signal<int>("count") and signal<float>("ratio") take the
same argument and hand back different types. A function that comes from a
module bound with as takes its type arguments after the path:
m::signal<int>("count").
Nothing in a bare signal("count") pins T, and that is not an error either: a
type parameter no argument pins and no type argument names is any, so the call
hands back a Signal<any> and the methods of an impl Signal<any> apply to it.
Name the argument to reach a different instantiation.
Generic structs¶
A struct names its type parameters after the struct name and uses them as field types.
struct Pair<K, V> {
key: K,
value: V,
}
fn main() {
let p = Pair<string, int>{ key: "width", value: 40 };
print(p.key, p.value);
}
Written without type arguments, a literal takes each parameter from the value in
the field declared with it, so Pair{ key: "width", value: 40 } is the same
type as the one above. A parameter that no field pins is any.
Type arguments nest, and a generic type is an ordinary type anywhere a type
goes: a field, a parameter, a return annotation. A type that comes from a
module bound with as keeps the alias in front of the name:
g::Cell<int>.
struct Cell<T> {
value: T,
}
struct Row {
cells: Cell<int>[],
}
fn deepen(c: Cell<int>) -> Cell<Cell<int>> {
return Cell<Cell<int>>{ value: c };
}
Generic enums¶
An enum declares type parameters the same way, and its variants carry them as payload types.
enum Slot<T> {
Filled(T),
Empty,
}
fn unwrap(s: Slot<int>) -> int {
match s {
Filled(x) => { return x; }
_ => { return 0; }
}
}
fn main() {
print(unwrap(Slot<int>::Filled(9)));
print(unwrap(Slot<int>::Empty));
}
Name the instantiation in front of the variant (Slot<int>::Filled(9)) to say
which one you mean. A bare Filled(9) resolves by variant name, which is enough
when only one instantiation of the enum exists in the program. From a module
bound with as the instantiation sits in the middle of the path,
g::Slot<int>::Filled(9), where a value is built and in a match arm alike.
A payload-less variant written bare pins nothing, so its type arguments are
any. It is accepted wherever a named instantiation is expected: as an argument
of a function, and as the payload of another variant, which is what lets a
recursive type be built without naming the instantiation at every leaf.
enum Tree<T> {
Leaf,
Node(Tree<T>, T, Tree<T>),
}
fn sum(t: Tree<int>) -> int {
match t {
Leaf => { return 0; }
Node(l, x, r) => { return sum(l) + x + sum(r); }
}
}
fn main() {
print(sum(Tree<int>::Node(Tree<int>::Node(Leaf, 1, Leaf), 2, Leaf)));
}
impl blocks¶
An impl block on a generic type names type arguments in its header, and there
are two kinds.
A block that introduces a parameter applies to every instantiation and binds the parameter for its methods:
A block that names concrete types applies to that instantiation alone, which is how one generic type gets different behaviour per type argument:
impl Signal<int> {
fn get(self) -> int {
return app::signal_get_int(self.name);
}
}
impl Signal<float> {
fn get(self) -> float {
return app::signal_get_float(self.name);
}
}
Every instantiated type gets each method once. A generic block and a concrete
block that both define get for Signal<int> is the same error as defining a
function twice. An impl on a generic type has to name its type arguments:
impl Cell alone is rejected.
Type parameters on a method¶
A method declares its own type parameters after its name, on top of whatever the
impl header binds for the receiver. They behave like a function's: leave them
off and each is inferred from the arguments, or name them at the call. One the
arguments do not mention is any when the call names nothing.
struct Store<T> {
seed: T,
}
impl Store<T> {
fn tagged<U>(self, extra: U) -> U {
return extra;
}
}
fn main() {
let s = Store<int>{ seed: 1 };
print(s.tagged("hi"));
print(s.tagged<string>("hi"));
}
Name the argument to choose which instantiation the body builds, since nothing in the call pins it:
struct Kind<T> {
n: int,
}
impl Kind<int> {
fn name(self) -> string {
return "int";
}
}
impl Kind<string> {
fn name(self) -> string {
return "string";
}
}
impl Store<T> {
fn name_of<U>(self) -> string {
return Kind<U>{ n: 0 }.name();
}
}
fn main() {
let s = Store<int>{ seed: 1 };
print(s.name_of<int>());
print(s.name_of<string>());
}
Leaving type arguments off¶
A missing type argument is never an error. A call falls back to inference, a
struct literal takes its arguments from its field values, a type parameter that
neither pins is any, and a generic type named in a type position without
arguments is the dynamic any slot:
Reach for the arguments when you want the check; leave them off while the shape is still moving.
When < is a comparison¶
Type arguments have no separate spelling, so < after a name is read as a type
argument list only when the whole list parses as types and closes with >
immediately followed by (, { or ::. Everything else stays the comparison
it has always been, including a < b, a < b && c > d and a < b > c.
After a . the rule is the same, narrowed to the one form a method call can
take: p.get<int>() is a call with a type argument, and only ( closes the
list.
The one shape that reads both ways is a comparison whose right-hand side is
parenthesised or braced, as in f(a < b, c > (d)). That parses as a call to
a with type arguments, and p.x < a > (b) likewise as a call to a method x.
Parenthesise the comparisons to keep them: f((a < b), (c > (d))).
Limits¶
Type parameters have no bounds: a parameter accepts any type, and a body that
does not work for the type it is given is reported against the instantiation
that produced it, not against the declaration. Choosing an impl block by an
exact type argument is the only form of specialisation.
Each distinct type argument produces its own instantiation, so a type whose
fields name a deeper instantiation of itself (struct L<T> { next: L<L<T>> })
has no end and is rejected.
Only methods that declare type parameters take type arguments. The built-in
methods on strings, arrays, maps and numbers are not generic and reject them,
and the standard library's collection methods (map, filter, reduce, and
the rest) declare none, so a type argument on one is reported the same as on
any plain function.