Generics
Generics let you write types and functions that work with any type, while keeping full type safety. Write it once, use it with anything—and pay no runtime cost.
Why Generics?
Without generics, you'd need separate implementations for every type:
type IntPair {
first i32
second i32
}
type StringPair {
first string
second string
}
type UserPair {
first User
second User
}
// ... and so on forever
That's tedious and error-prone. With generics:
type Pair<T> {
first T
second T
}
// Positional - if you provide all fields in order, labels are optional
$ints = Pair { 1, 2 };
$strings = Pair { "hello", "world" };
$users = Pair { $alice, $bob };
// Labeled - explicit field names
$point = Pair { first: 10, second: 20 };
One definition, infinite uses. The compiler generates the specialized versions for you.
Generic Types
Add type parameters with angle brackets:
type Box<T> {
value T
}
type Result<T, E> {
// Either a success value or an error
}
type HashMap<K, V> {
// Keys of type K, values of type V
}
Using Generic Types
// Positional initialization (all fields in order)
$box = Box { 42 }; // Box<i32>
// Labeled initialization
$box: Box<i32> = Box { value: 42 };
// Multiple parameters
$result: Result<User, Error> = Ok($user);
$map: HashMap<string, i32> = HashMap::new();
When you provide all fields in declaration order, labels are optional. This keeps simple types concise while still allowing explicit labels for clarity.
Methods on Generic Types
type Container<T> {
items Vec<T>
fn add<T>($item T) {
$this.items.push($item);
}
fn get<T>($index i32): ?&T {
return $this.items.get($index);
}
fn len<T>: i32 {
return $this.items.len();
}
}
The <T> on each method means "for any type T that this Container holds."
Type Constraints
Often you need types that support certain operations. Use trait bounds:
// T must be displayable
type Labeled<T: Display> {
label string
value T
}
// T must be comparable
type SortedList<T: Ord> {
items Vec<T>
}
// Multiple constraints
type Cache<K: Hash + Eq, V: Clone> {
data HashMap<K, V>
}
Without constraints, you can only do things that work on any type (store it, move it, drop it). With constraints, you can use the trait's methods.
Where Clauses
For complex constraints, use where:
type Processor<I, O>
where
I: Deserialize,
O: Serialize + Send
{
input: PhantomData<I>
output: PhantomData<O>
}
This is clearer than cramming everything into the angle brackets.
Monomorphization
Here's the magic: generics have zero runtime cost.
When you use a generic type, the compiler generates a specialized version for that specific type:
// You write:
type Pair<T> {
first T
second T
}
$int_pair = Pair { first: 1, second: 2 };
$str_pair = Pair { first: "a", second: "b" };
// Compiler generates:
type Pair_i32 {
first i32
second i32
}
type Pair_string {
first string
second string
}
This is called monomorphization. The result is as fast as if you'd written specialized types by hand.
Trade-offs
Monomorphization gives you speed but increases binary size. Each unique instantiation generates new code:
// Each of these creates separate compiled code:
Vec<i32>
Vec<string>
Vec<User>
Vec<Order>
For most programs this is fine. If binary size is critical, consider trait objects for dynamic dispatch (covered in the OOP chapter).
Type Inference with Generics
Coco's type inference usually figures out generic parameters:
$numbers = vec![1, 2, 3]; // Vec<i32>
$names = vec!["a", "b"]; // Vec<string>
$pairs = vec![(1, "one")]; // Vec<(i32, string)>
Sometimes you need to be explicit:
// Ambiguous - what type of Vec?
$empty = Vec::new(); // Error: cannot infer type
// Solutions:
$empty: Vec<i32> = Vec::new();
$empty = Vec::<i32>::new();
The turbofish syntax ::< > specifies type parameters on function calls.
Generic Enums
Enums can be generic too:
enum Option<T> {
Some(T),
None
}
enum Result<T, E> {
Ok(T),
Err(E)
}
// Usage
$maybe: Option<i32> = Some(42);
$result: Result<User, Error> = Ok($user);
These two are so fundamental they're built into Coco.
Associated Types
Sometimes a type parameter is determined by the implementing type, not the user. Use associated types:
trait Iterator {
type Item; // Associated type
fn next(&$this): Option<Self::Item>;
}
// The implementor chooses Item
type Counter {
current i32
max i32
}
impl Iterator for Counter {
type Item = i32; // Counter iterates over i32s
fn next(&$this): Option<i32> {
if $this.current < $this.max {
$result = $this.current;
$this.current += 1;
return Some($result);
}
return None;
}
}
Compare to using a type parameter:
// With type parameter - caller chooses
trait Iterator<T> {
fn next(&$this): Option<T>;
}
// With associated type - implementor chooses
trait Iterator {
type Item;
fn next(&$this): Option<Self::Item>;
}
Use associated types when there's one natural choice per implementing type.
Phantom Types
Sometimes you want a type parameter that doesn't appear in any field—just for type checking:
type Id<T> {
value i64
_phantom PhantomData<T>
}
type User {}
type Order {}
fn get_user($id Id<User>): User { ... }
fn get_order($id Id<Order>): Order { ... }
$user_id: Id<User> = Id { value: 42, _phantom: PhantomData };
$order_id: Id<Order> = Id { value: 42, _phantom: PhantomData };
get_user($user_id); // OK
get_user($order_id); // Error: expected Id<User>, got Id<Order>
The T doesn't affect the runtime representation—both are just i64—but the compiler prevents you from mixing them up.
Bounded Quantification
You can express relationships between type parameters:
// T and U might be different types, but both Displayable
fn show_both<T: Display, U: Display>($a T, $b U) {
echo "{$a} and {$b}";
}
// U must be the same as T's associated Output type
fn apply<T, U>($f T, $x U): T::Output
where
T: Fn(U) -> T::Output
{
return $f($x);
}
Default Type Parameters
Provide defaults for convenience:
type HashMap<K, V, H = DefaultHasher> {
// ...
}
// Uses default hasher
$map: HashMap<string, i32> = HashMap::new();
// Custom hasher
$map: HashMap<string, i32, FastHasher> = HashMap::new();
Comparison to Other Languages
vs C++ Templates
Similar monomorphization approach, but Coco has:
- Trait bounds (C++ concepts are newer and less integrated)
- Better error messages (constraints checked at definition, not instantiation)
- No SFINAE complexity
vs Java Generics
Java uses type erasure—generic info is lost at runtime:
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
// At runtime, both are just ArrayList
Coco monomorphizes—full type info preserved, better performance, no boxing.
vs Go Generics
Go added generics in 1.18 with similar syntax. Both use monomorphization. Coco's trait system is more expressive than Go's interface constraints.
vs TypeScript
TypeScript generics are structural and erased at runtime (it's just JavaScript). Coco generics are nominal and fully preserved.
Best Practices
Start concrete, generalize later. Write the specific version first. Once you need it for multiple types, add generics.
Use meaningful parameter names. T is fine for one parameter. Use Key, Value, Input, Output for clarity with multiple parameters.
Constrain appropriately. Don't over-constrain (limiting flexibility) or under-constrain (getting unhelpful "doesn't implement X" errors at use sites).
Prefer associated types for "output" types. If the type is determined by the implementation, not the caller, use an associated type.
Document constraints. Explain why each bound is needed:
/// A sorted collection that maintains order.
///
/// K must be Ord for sorting, Hash + Eq for deduplication.
type SortedSet<K: Ord + Hash + Eq> {
// ...
}