Generics
Generics
Type parameters are declared in angle brackets after the name:
[public] class Array<T> {
buffer: pointer<T>
count: i64
}
[public] fn serialize<T: impl Serialize>(value: T) => JsonValue {
// ...
}Bounds
A bound is written with impl inside the parameter list, and a parameter may
carry several:
[public] class Map<KT: impl Hash, impl Equals, VT> {
// ...
}Here KT must implement both Hash and Equals, while VT is unconstrained.
The analyzer rejects a call that supplies a type argument failing a bound, and
it does so at the call site rather than inside the generic body.
Erasure
Generics are erased. A generic body compiles once, no matter how many types it is used with, and calls to a bound's methods dispatch through a witness table built for the concrete type at the use site.
Two consequences are worth knowing:
- Compile time and binary size do not grow with the number of instantiations, because there is only ever one copy of the body.
- A type argument is only usable through its bounds. Code inside a generic cannot call a method the bounds do not promise, because the body was compiled without knowing the concrete type.
This is also what makes prebuilt distribution of a library possible: one compiled object serves every instantiation a consumer might write.
Carrying raw values with Box
Erasure has one practical limit: a type argument is only usable through its
bounds, so a raw machine value cannot travel in a generic directly. Box<T> is
the escape hatch.
[public] class Box<T> {
constructor(value: T)
[public] fn get() => T
[mutates] fn set(value: T)
}Box is the one generic the compiler specializes per type, so it can hold what
an erased generic cannot: a sized scalar, an opaque handle, a pointer<T>.
let handles: Array<Box<opaque>> = new Array<Box<opaque>>()
handles.push(new Box<opaque>(native_handle))Ordinary objects need no box; they pass through generics unchanged.
Inferring type arguments
Type arguments are usually inferred rather than written out. They come from two places, and either is enough.
From the arguments of the call:
let lengths = names.map(closure(name: string) => number {
return name.size()
})U is inferred as number from the closure's return type.
From the type the result is expected to have:
let names: Array<string> = new Array()The declared type says the result is an Array<string>, so new Array() needs
no <string> of its own. The same applies anywhere the expected type is known,
including a field with a declared type, a parameter being passed to, and a
return in a function with a declared return type:
fn empty_names() => Array<string> {
return new Array()
}When neither source determines the parameters, the analyzer says so and asks for them outright rather than guessing. Writing them is always allowed:
let names = new Array<string>()