Generics let one declaration operate over a family of types while preserving static checking.
fn identity[T](value: T): T {
return value
}Type parameters use square brackets.
Types
Structs, enums, aliases, traits, and functions can declare type parameters.
struct Pair[A, B] {
first: A
second: B
}
enum Tree[T] {
Leaf(T)
Branch([]Tree[T])
}
type Predicate[T] = fn(T) -> boolEach use supplies the declared number of arguments.
entry: Pair[string, int] = Pair {
first: "port"
second: 8080
}Different arguments create different static instantiations. Pair[int, bool]
does not share a type with Pair[float, string].
Inference
Calls normally infer type arguments from their parameters and expected result.
number := identity(42)
text := identity("atoll")Each call gets an independent substitution. A prior call with int does not
permanently specialize the declaration for later callers.
Explicit calls
When inference cannot recover an argument, place type arguments in square brackets before the call parentheses.
size := size_of[Packet]()
empty := List.new[string]()
converted := source.convert[Target]()Without following (...), value[index] remains an indexing expression. The
call shape makes the generic argument list unambiguous.
The old ::<T> and <T> call spellings are not the current convention.
Bounds
A bound lists behavior required from a type parameter.
fn larger[T](left: T, right: T): T
where T: Comparable[T] {
if left > right { left } else { right }
}+ combines requirements.
fn render[T](value: T): string
where T: Display + Clone {
return value.to_string()
}The checker resolves calls and operators inside the generic body through those bounds. Missing behavior is reported at the use that requires it.
Bounds may also appear inline where the declaration grammar permits. A
where clause is clearer when several parameters or requirements interact.
Self
Inside a trait, Self denotes the implementing type.
trait Merge {
fn merge(self, other: Self): Self
}Inside an implementation, declared type parameters and Self are substituted
with the implementation’s concrete target.
Blanket implementations
An implementation can itself be generic.
trait Printable: Display + Debug {}
impl[T] Printable for T
where T: Display + Debug {}The coherence checker rejects implementations that overlap without a provable disjoint constraint. Generic convenience cannot make method selection ambiguous.
Opaque types
impl Trait hides a concrete type behind a trait contract.
fn area(shape: impl Shape): float {
return shape.area()
}In parameter position it accepts a concrete argument satisfying the trait. In return position, a declaration produces one hidden concrete type anchored to that function.
Two functions returning impl Trait do not automatically expose the same
opaque type even if their current bodies choose the same concrete
representation.
Atoll currently uses static dispatch. There is no dyn Trait type or
user-visible vtable contract.
Monomorphization
The compiler specializes generic functions, methods, closures, and layouts for reachable concrete arguments. This supports direct calls and concrete field layouts.
Monomorphization is an implementation strategy, not permission to inspect a type parameter without a bound. Generic source is checked as generic source before specialization.
Recursive generics
Recursive uses must still have a finite representation.
enum Node[T] {
Empty
Children([]Node[T])
}The List introduces managed indirection. A payload containing Node[T]
directly with no representational break is rejected.
Type matching
The current compiler has a compile-time type match used by specialized generic code.
fn width_code[T](): int {
return match T {
u8 => 1
u16 => 2
else => 0
}
}This is compile-time specialization over T, not a runtime value is Type
test.