Generics
Generics let you write functions that work with any type, while keeping full type safety and zero runtime cost. Write once, use with anything.
Basic Generic Functions
fn identity<T>($x T): T {
return $x;
}
$num = identity(42); // T = i32
$text = identity("hello"); // T = string
The <T> declares a type parameter. The compiler figures out what T is from how you call the function.
Multiple Type Parameters
fn pair<A, B>($a A, $b B): (A, B) {
return ($a, $b);
}
$p = pair(1, "one"); // (i32, string)
Type Constraints
Often you need types that support certain operations. Use traits as constraints:
// T must implement Display
fn print_value<T: Display>($value T) {
echo "{}", $value;
}
// Multiple constraints
fn compare_and_show<T: Comparable + Display>($a T, $b T) {
if $a > $b {
echo "{$a} is greater";
} else {
echo "{$b} is greater";
}
}
The where Clause
For complex constraints, use where:
fn process<T, U>($input T, $transform U): T
where
T: Clone + Default,
U: Fn(T) -> T
{
$result = $transform($input.clone());
return $result;
}
Common Constraints
| Trait | What It Means |
|---|---|
Copy |
Can be copied bitwise |
Clone |
Can be cloned explicitly |
Display |
Can be formatted for display |
Debug |
Can be formatted for debugging |
Comparable |
Supports comparison operators |
Default |
Has a default value |
Hash |
Can be hashed (for hash maps) |
Monomorphization
Here's the magic: generics have zero runtime cost. The compiler generates specialized versions for each concrete type you use:
fn add<T: Add>($a T, $b T): T {
return $a + $b;
}
// When you call:
add(1, 2); // Compiler generates add_i32
add(1.5, 2.5); // Compiler generates add_f64
The generated code is identical to what you'd write by hand. No boxing, no virtual dispatch, no overhead.
Generic Structs
Types can also be generic:
type Pair<T> {
first T
second T
fn new<T>($first T, $second T): Pair<T> {
return Pair { first: $first, second: $second };
}
fn swap<T>(&$this) {
$temp = $this.first;
$this.first = $this.second;
$this.second = $temp;
}
}
$p = Pair::new(1, 2);
$p.swap();
echo $p.first, $p.second; // 2, 1
Implementing Traits for Generic Types
impl<T: Display> Display for Pair<T> {
fn fmt(&$this, $f &Formatter) {
write!($f, "({}, {})", $this.first, $this.second);
}
}
Type Inference
Usually you don't need to specify type parameters—the compiler infers them:
$numbers = vec![1, 2, 3]; // Vec<i32>
$names = vec!["a", "b"]; // Vec<string>
But sometimes you need to be explicit:
// Ambiguous - could be any numeric type
$empty = Vec::new(); // Error: cannot infer type
// Explicit
$empty = Vec::<i32>::new(); // Ok
$empty: Vec<i32> = Vec::new(); // Also ok
Static vs Dynamic Dispatch
Generics use static dispatch—the compiler knows the exact type at compile time:
fn process<T: Processor>($item T) {
$item.process(); // Direct call, no indirection
}
For dynamic dispatch (when you genuinely need runtime polymorphism), use trait objects:
fn process_any($item dyn Processor) {
$item.process(); // Virtual call through vtable
}
Use generics by default. They're zero-cost—the compiler generates direct calls with full optimization. Only use dyn when you have a specific reason: plugin systems, FFI, or truly unknown types at runtime. The dyn keyword is an explicit opt-in to runtime costs (heap allocation, vtable lookup, no inlining).
Common Patterns
Swap
fn swap<T>($a &T, $b &T) {
$temp = *$a;
$a = $b;
*$b = $temp;
}
Min/Max
fn min<T: Comparable>($a T, $b T): T {
if $a < $b { $a } else { $b }
}
Option Combinators
fn map_option<T, U>($opt ?T, $f fn(T): U): ?U {
match $opt {
Some($v) => Some($f($v)),
None => None
}
}
Builder Pattern
type Builder<T> {
value T
fn new<T: Default>(): Builder<T> {
return Builder { value: T::default() };
}
fn with<T>($this Builder<T>, $f fn(&T)): Builder<T> {
$f(&$this.value);
return $this;
}
fn build<T>($this Builder<T>): T {
return $this.value;
}
}
Limitations
No specialization. You can't provide a more efficient implementation for specific types.
No variadic generics. You can't have a variable number of type parameters.
Constraints must be explicit. The compiler won't infer what traits you need.
Best Practices
Start concrete, then generalize. Write the function for a specific type first. Once it works, add generics if you need them.
Use meaningful type parameter names. T is fine for one parameter, but use Key, Value, Input, Output for clarity.
Prefer static dispatch. Generics are zero-cost. Only use dyn when you genuinely need runtime polymorphism (plugins, FFI, unknown types).
Don't over-generalize. If a function only ever uses one type, don't make it generic just for the sake of it.
Document constraints. Explain why you need each constraint, especially for complex bounds.