Generics
A type can leave one of the types it uses open, and have it chosen where the type is named. The open ones are its type parameters, written in angle brackets after the name.
pub type Box<T> {
pub field value: T;
}
T is a type like any other inside the declaration. It names a field, a
parameter, a return type, or the element type of an array.
pub type Box<T> {
pub field value: T;
pub field spares: T[];
pub fn get() -> T {
return self.value;
}
}
A type parameter is a name only inside the type that declares it. Nothing
outside can refer to T.
Filling them in
Naming the type anywhere else means choosing what each parameter stands for.
let b = new Box<i32> { value: 41, spares: new i32[0], };
let n = b.get() + 1; // an i32, not a T
The choice travels with the type, so b.value is an i32 and assigning a
string to it is an error.
A type takes exactly as many arguments as it declares parameters. Naming it with the wrong number is an error, and so is naming it with none.
let a: Box = b; // Error: 'Box' takes 1 type argument, but 0 were given
let c: Box<i32, i32> = d; // Error: 'Box' takes 1 type argument, but 2 were given
let e: i32<bool> = 1; // Error: 'i32' takes no type arguments
More than one
Parameters are separated by commas, and each is chosen independently.
pub type Pair<A, B> {
pub field first: A;
pub field second: B;
}
let p = new Pair<i32, string> { first: 7, second: "seven", };
An argument can itself be a type with arguments, to any depth.
let n = new Box<Box<i32>> { value: new Box<i32> { value: 3, }, spares: new Box<i32>[0], };
println(itoa(n.value.value)); // 3
A type may also name itself, which is what a list or a tree needs.
pub type Node<T> {
pub field value: T;
pub field next: Node<T>;
}
Interfaces
An interface takes parameters the same way.
pub interface Holder<T> {
fn held() -> T;
}
An impl block can fill them in with a type, which is what a non-generic type
implementing a generic interface looks like.
pub type Counter {
pub field total: i32;
}
impl Holder<i32> for Counter {
pub fn held() -> i32 {
return self.total;
}
}
It can also fill them in with the parameters of the type it is implementing for. Write those after the target, matching what the type declares.
impl Holder<T> for Box<T> {
pub fn held() -> T {
return self.value;
}
}
The target may be written bare when nothing needs to name its parameters. These two say the same thing:
impl Describable for Box { }
impl Describable for Box<T> { }
Either way the block covers every filling in of the type at once, so a value can be used through the interface with its arguments chosen.
pub fn read(holder: Holder<i32>) -> i32 {
return holder.held();
}
println(itoa(read(new Box<i32> { value: 5, })));
Constraints
A parameter with nothing said about it can only be stored, passed and handed back, because nothing is known about what it can do. A constraint says it implements an interface, and everything that interface declares becomes available on a value of that type.
pub interface Describable {
fn describe() -> string;
}
pub type Box<T: Describable> {
pub field value: T;
pub fn show() -> string {
return self.value.describe();
}
}
Filling the parameter in with a type that does not implement the interface is an error, reported where the type is named rather than inside the declaration.
let b = new Box<i32> { value: 1, };
// Error: 'i32' does not implement 'Describable', which 'T' is constrained to
Only an interface can be a constraint. Naming a type is an error, since a type has no implementers.
A constraint may name the parameters it constrains, which is how an interface says something about the type implementing it.
pub interface Comparable<T> {
fn compare_to(other: T) -> i32;
}
pub type Smallest<T: Comparable<T>> {
pub mut field current: T;
pub fn add(item: T) -> void {
if item.compare_to(self.current) < 0 {
self.current = item;
}
}
}
Comparable<T> is filled in along with the parameter, so Smallest<Score>
requires Score to implement Comparable<Score> rather than Comparable<T>.
impl Comparable<Score> for Score {
pub fn compare_to(other: Score) -> i32 {
return self.points - other.points;
}
}
Functions
A function takes type parameters the same way, and they are worked out from the arguments rather than written at the call.
pub fn first<T>(items: T[]) -> T {
return items[0];
}
let number = first(new int[] { 3, 1, 2, }); // an i32
let word = first(new string[] { "a", "b", }); // a string
A parameter that appears nowhere in the argument types cannot be worked out. Name it at the call instead.
pub fn empty<T>() -> T[] {
return new T[0];
}
let a = empty(); // Error: 'T' cannot be worked out from the arguments
let b = empty<i32>(); // an i32[]
Type arguments are written the same way to reach a static method through the type that declares it.
let smallest = Smallest<i32>::new(41);
< still compares two values everywhere else. It opens a type argument list
only when a matching > is followed by a call or a ::, so f(a < b, c > d)
stays the two comparisons it looks like.
A method may declare its own, separately from the type's. They may even share a name, in which case the method's wins for as long as it lasts.
pub type Box<T> {
pub field value: T;
pub fn map<U>(other: U) -> U {
return other;
}
}
Constraints work as they do on a type.
pub fn show<T: Describable>(value: T) -> string {
return value.describe();
}
show(42); // Error: 'i32' does not implement 'Describable'
One filling in at a time
impl Describable for Box<i32> is an error. There is no way to give one filling
in of a type behaviour that the others do not have.
This is not only a missing feature. A generic type is emitted once, with the
parameters left open, and its interfaces are fixed where it is declared. Giving
Box<i32> an interface that Box<string> lacks would mean emitting a separate
type per filling in, and answering what happens when a general impl and a
specialized one both apply. What that example usually wants is a
constraint.
Not yet
Generics do not carry variance, defaults, or more than one constraint per parameter. A union will take type parameters the same way once unions exist.