7. Generics
Use generics when you want the same logic to work for many types. rok generics are compiled separately for each concrete type they're called with (monomorphization), so they run as fast as hand-written per-type functions.
Generic functions
Declare type parameters in <T> after the function name and use them in the parameter and return types. At the call site you usually don't write the type; the compiler infers it from the arguments.
fn first_or<T>(xs: List<T>, fallback: T) -> T {
xs.first() ?? fallback
}
println(f"{first_or([10, 20], 0)}");
println(first_or(["a", "b"], "-"));
let empty: List<f64> = [];
println(f"{first_or(empty, 1.5)}");
10
a
1.5
Within one call, T is a single type. Passing different types, as in first_or([1, 2], "x"), is a compile error.
Trait bounds
With an unbounded T, all you can do is move values around. To compare or add them, require the needed traits with a bound such as T: Ord. Join multiple bounds with +. The compiler checks a generic body against its bounds alone, so the body cannot break no matter which type it's called with.
fn largest<T: Ord>(xs: List<T>) -> T? {
let best = xs.first()?;
for x in xs {
if x > best { best = x; }
}
best
}
println(f"{largest([3, 9, 2]):?}");
println(f"{largest(["pear", "apple"]):?}");
let none_here: List<i32> = [];
println(f"{largest(none_here):?}");
9
"pear"
none
With T: Zero, you can also call static methods on the type parameter, such as T::zero().
fn sum<T: Zero + Add<Output = T>>(xs: List<T>) -> T {
let acc = T::zero();
for x in xs { acc = acc + x; }
acc
}
println(f"{sum([1, 2, 3])} {sum([0.5, 0.25])}");
6 0.75
The body is checked against its bounds
A generic body is type-checked once, on its own, before any call is looked at. Inside it, T supports only what its bounds promise, plus what every type supports: moving, storing into any, forming T | none, and is. Forgetting a bound is an error in the body, even if every call passes a type that would work.
fn double<T>(x: T) -> T { x + x }
error[E200]: operator `+` on `T` needs a `Add` bound on `T` (spec 2.10.9)
--> main.rok:1:27
|
1 | fn double<T>(x: T) -> T { x + x }
| ^^^^^
With the bound added, the same body compiles for every type that has Add. The operations that need no bound work without one.
fn double<T: Add<T, Output = T>>(x: T) -> T { x + x }
fn keep<T>(x: T, yes: bool) -> T | none { if yes { x } else { none } }
fn boxed<T>(x: T) -> any { x }
println(f"{double(21)} {double(1.5)}");
println(f"{keep("a", true):?} {keep(1, false):?}");
println(f"{boxed(3) is i32}");
42 3
"a" none
true
Because of this rule, a library's generic function that compiles for you compiles for everyone who calls it; an error never surfaces at a distant call site. Trait default methods are checked the same way, with Self known only through the trait. A function with untyped parameters (an implicit generic, below) is the exception.
A bound can name the same trait twice with different parameters. Each use in the body picks the bound whose parameter matches the argument's type, just as an operator picks among a type's impls.
struct Meters { v: f64; }
impl Add<i32> for Meters { type Output = Meters; fn add(self, rhs: i32) -> Meters { Meters { v: self.v + rhs as f64 } } }
impl Add<f64> for Meters { type Output = Meters; fn add(self, rhs: f64) -> Meters { Meters { v: self.v + rhs } } }
fn nudge<T: Add<i32, Output = T> + Add<f64, Output = T>>(x: T) -> T {
(x + 1) + 0.5 // Add<i32>, then Add<f64>
}
println(f"{nudge(Meters { v: 2.0 }).v}");
3.5
where clauses
When bounds get long, moving them to a where clause at the end of the signature makes it easier to read. The meaning is the same.
fn join_all<T>(xs: List<T>, sep: str) -> str
where
T: Display,
{
let out = StrBuf::new();
for i in 0..xs.len() {
if i > 0 { out.push_str(sep); }
out.push_str(f"{xs[i]}");
}
out.to_str()
}
println(join_all([1, 2, 3], ", "));
println(join_all([true, false], " / "));
1, 2, 3
true / false
Generic structs and impl
Structs and enums can take type parameters too. When adding methods, declare the parameters on the impl as well, as in impl<A, B> Pair<A, B>.
struct Pair<A, B> { first: A; second: B; }
impl<A, B> Pair<A, B> {
fn swap(self) -> Pair<B, A> {
Pair { first: self.second, second: self.first }
}
}
let p = Pair { first: 1, second: "one" };
let q = p.swap();
println(f"{q.first} {q.second}");
one 1
Associated types
To let each implementation of a trait pick one type, declare an associated type type Item;. Inside the trait, refer to it as plain Item; in generic code outside, as C::Item. The standard Iterator works this way (next chapter).
trait Container {
type Item;
fn get(self, i: usize) -> Item | none;
fn first(self) -> Item | none { self.get(0) }
}
struct Shelf { books: List<str>; }
impl Container for Shelf {
type Item = str;
fn get(self, i: usize) -> str? { self.books.get(i) }
}
fn first_of<C: Container>(c: C) -> C::Item | none { c.first() }
let s = Shelf { books: ["Dune", "Emma"] };
println(f"{first_of(s):?} {s.get(5):?}");
"Dune" none
It is an associated type rather than a type parameter because Item is fixed to exactly one type for a given implementor. Users don't need to spell out Shelf's element type.
Advanced: const generics
You can skip this section on a first read. A parameter can also be a compile-time value instead of a type. Use this when a value is part of a type, like the length of a fixed-size array [T; N].
fn total<const N: usize>(a: [i32; N]) -> i32 {
let s = 0;
for i in 0..N { s += a[i]; }
s
}
let a: [i32; 4] = [1, 2, 3, 4];
let z: [f64; 3] = [0.5; 3];
println(f"{total(a)} {a.len()} {z:?}");
10 4 [0.5, 0.5, 0.5]
const parameters can only be integers, bool, or char.
Generics and any
Generics are static. When you instead need a slot whose value's type may change at runtime, write any explicitly. Narrow any values with is before use. Dynamic dispatch costs appear only where you wrote any.
let v: any = 1;
println(f"{v is i32}");
v = "text";
if v is str {
println(f"now a string, length {v.len()}");
}
true
now a string, length 4
The spec defines a function with omitted parameter types, fn show_sum(x, y), as an implicit generic, not as any. Bounds are inferred from how the body uses the parameters, and the function is monomorphized at each call site.
fn show_sum(x, y) { println(f"{x + y}"); } // implicit generic: T: Add<U>
show_sum(1, 2); // 3
show_sum(1.5, 2.0); // 3.5
3
3.5
Summary
- Declare generics with
fn f<T>(..),struct S<A, B>, andimpl<A, B> S<A, B>. Type arguments are usually inferred. - Bounds like
T: Ord + Clonedetermine which operations the body may use. Move long ones to awhereclause. - An associated type
type Item;is fixed to one type per implementation and is writtenC::Itemfrom outside. const N: usizetakes a value as a type parameter.- Generics are static (monomorphized);
anyis a dynamic slot that exists only where you write it.