Generics
Type parameters, interface bounds, const generic arguments, and type packs.
Generics let one declaration work with many concrete types. Zynx specializes generic functions, structs, interfaces, and type aliases for the concrete arguments used by a program.
struct Box<T> {
value: T,
}
fn id<T>(value: T) -> T {
return value;
}
fn main() {
let boxed = Box<i32> { value: 41 };
let answer = id(boxed.value + 1);
assert answer == 42;
}
Generic arguments can be written explicitly, but the compiler often infers them from constructor and function arguments.
struct Container<T> {
value: T,
init(self, value: T) {
self.value = value;
}
}
fn main() {
let count = Container(42);
let label = Container("ok");
assert count.value == 42;
assert label.value == "ok";
}
Struct literals can infer generic arguments from their fields:
struct Wrap<T> {
value: T,
}
fn main() {
let wrapped = Wrap { value: 7 };
assert wrapped.value == 7;
}
Use an interface bound when generic code needs operations beyond moving, copying, or passing values through.
fn double<T: Integer>(value: T) -> T {
return value + value;
}
fn main() {
assert double<i32>(21) == 42;
assert double<u8>(5 as u8) == (10 as u8);
}
Integer, Signed, Unsigned, and Copy are compiler-provided marker
interfaces from std.builtin. Integer excludes bool; Signed and
Unsigned imply Integer. User structs cannot opt into the numeric markers.
For user-defined capability interfaces, see
Interfaces. For the ownership meaning of
Copy, see Move, Copy, and Clone.
A generic parameter declared with a plain integer type is a const generic parameter. It accepts integer literals in generic argument lists:
struct SizedArray<T, N: usize> {
items: [T, N],
}
fn len<T, N: usize>(items: [T, N]) -> usize {
_ = items[0];
return N;
}
fn main() {
let explicit = SizedArray<i32, 3> { items: [1, 2, 3] };
let inferred = SizedArray { items: [4, 5] };
assert len(explicit.items) == 3;
assert len(inferred.items) == 2;
}
Const generic arguments must be integer literals or another const generic
parameter, and the literal must fit the parameter type. A type parameter expects
a type argument, so Box<3> is rejected. A const parameter expects a numeric
argument, so SizedArray<i32, i32> is rejected.
Const generics are most commonly used with fixed-size arrays, where [T, N]
means an array whose element type and length are both part of the type.
Generic type aliases can also use const parameters:
type Fixed<T, N: usize> = [T, N];
fn first<T, N: usize>(items: Fixed<T, N>) -> T {
return items[0];
}
Type packs represent a variable-length list of types. A pack parameter uses
..., must be the final generic parameter, and must be expanded only in
supported positions.
fn tuple<T...>(xs: T...) -> (T...) {
return (xs...);
}
fn main() {
let t = tuple(1, true, "ok");
assert t[0] == 1;
assert t[1];
assert t[2] == "ok";
}
Packs can also forward callable signatures:
fn call<R, T...>(f: (T...) -> R, xs: T...) -> R {
return f(xs...);
}
Const generic packs are not supported.
Type packs are supported only as the final generic parameter of function, method, operator-method, intrinsic-function, and struct declarations. They are not supported on type aliases, interfaces, enums, unions, error sets, or const generic parameters. A declaration may have at most one type pack, and it must be final.
Valid expansion positions are final function parameter types, tuple types, callable parameter lists, generic argument lists for pack-backed declarations, tuple expressions, and call argument forwarding. Pack expansion is not valid as an array element type, array size, error set, const generic argument, or arbitrary expression.
- Generic parameters may be types, const integer parameters, or one final type pack.
- Type parameters can have interface bounds such as
T: CopyorT: HasValue<i32>. - Const parameters such as
N: usizecannot have interface bounds. - Const generic values may be integer literals or in-scope const generic
parameter names; type-level arithmetic such as
N + 1is not current syntax. - Generic function arguments are inferred from call arguments when possible.
- Generic struct literals can infer their arguments from field values.
- A method generic parameter cannot shadow a generic parameter from the enclosing struct.
See Function Overloading for how generic candidates participate in overload resolution.