Polymorphism
Polymorphism lets you write code that works with multiple types. Coco gives you two flavors: static (generics) and dynamic (trait objects).
Static Polymorphism with Generics
When you know all the types at compile time, use generics:
trait Drawable {
fn draw;
}
fn render<T: Drawable>($item T) {
$item.draw();
}
type Circle {
radius f64
@Drawable
fn draw {
echo "Drawing circle with radius {$this.radius}";
}
}
type Square {
side f64
@Drawable
fn draw {
echo "Drawing square with side {$this.side}";
}
}
$circle = Circle { radius: 5.0 };
$square = Square { side: 3.0 };
render($circle); // Drawing circle with radius 5
render($square); // Drawing square with side 3
The compiler generates specialized versions of render for each type. Zero runtime overhead.
Dynamic Polymorphism with Trait Objects
When you need to work with different types at runtime, use trait objects:
fn render_any($item dyn Drawable) {
$item.draw();
}
// Can store different types together
$shapes: [dyn Drawable] = [
Circle { radius: 5.0 },
Square { side: 3.0 }
];
for $shape in $shapes {
$shape.draw();
}
The dyn keyword indicates dynamic dispatch—method calls go through a vtable at runtime. The compiler handles storage automatically.
When to Use Which
Use Generics When:
- All types are known at compile time
- You need maximum performance
- You want the compiler to check everything
// Fast - no runtime dispatch
fn max<T: Comparable>($a T, $b T): T {
if $a > $b { $a } else { $b }
}
Use Trait Objects When:
- Types are determined at runtime
- You need heterogeneous collections
- You want smaller binary size (one function, not many specialized versions)
// Flexible - can hold any Drawable
type Canvas {
shapes [dyn Drawable]
fn add($item dyn Drawable) {
$this.shapes.push($item);
}
}
Trait Object Limitations
Not all traits can be made into trait objects. The trait must be "object safe":
Allowed:
- Methods with
&selfor&mut self - Methods that return
Selfby reference
Not allowed:
- Methods with generic parameters
- Methods that return
Selfby value - Static methods
// Object safe
trait Drawable {
fn draw(&$this);
fn bounds(&$this): Rectangle;
}
// NOT object safe
trait Cloneable {
fn clone(&$this): Self; // Returns Self by value
}
Enum-Based Polymorphism
For a fixed set of variants, enums can be better than trait objects:
enum Shape {
Circle { radius f64 },
Square { side f64 },
Rectangle { width f64, height f64 }
}
type Shape {
fn draw {
match $this {
Circle { radius } => echo "Circle r={radius}",
Square { side } => echo "Square s={side}",
Rectangle { width, height } => echo "Rect {width}x{height}"
}
}
fn area: f64 {
match $this {
Circle { radius } => 3.14159 radius radius,
Square { side } => side * side,
Rectangle { width, height } => width * height
}
}
}
Advantages:
- No heap allocation
- Exhaustive matching
- All variants visible
Disadvantages:
- Can't add variants without changing the enum
- All variants must be known upfront
Generic Collections
Collections in Coco are generic:
// Homogeneous - all same type
$numbers: [i32] = [1, 2, 3];
$names: [string] = ["Alice", "Bob"];
// Heterogeneous - different types implementing same trait
$drawables: [dyn Drawable] = [
Circle { radius: 1.0 },
Square { side: 2.0 }
];
Polymorphic Structs
Types can be generic over their fields:
type Container<T> {
value T
fn get<T>(&$this): &T {
return &$this.value;
}
fn map<T, U>($this Container<T>, $f fn(T): U): Container<U> {
return Container { value: $f($this.value) };
}
}
$num = Container { value: 42 };
$doubled = $num.map(($x) { $x * 2 });
echo $doubled.get(); // 84
Type Erasure Patterns
Sometimes you want to hide the concrete type entirely:
trait Handler {
fn handle($request Request): Response;
}
type Router {
handlers HashMap<string, dyn Handler>
fn add($path string, $handler dyn Handler) {
$this.handlers.insert($path, $handler);
}
fn dispatch($path string, $request Request): ?Response {
match $this.handlers.get($path) {
Some($handler) => Some($handler.handle($request)),
None => None
}
}
}
The router doesn't know or care what concrete types the handlers are.
Performance Comparison
// Static dispatch - inline, no overhead
fn process_static<T: Process>($items [T]) {
for $item in $items {
$item.process(); // Direct call
}
}
// Dynamic dispatch - vtable lookup per call
fn process_dynamic($items [dyn Process]) {
for $item in $items {
$item.process(); // Indirect call through vtable
}
}
The dynamic version has a small cost per call (typically a few nanoseconds). For hot loops, this can add up. For most code, it's negligible.
Combining Approaches
You can mix static and dynamic polymorphism:
fn process_all<T: Drawable>($static_items [T], $dynamic_items [dyn Drawable]) {
// Static dispatch for known type
for $item in $static_items {
$item.draw();
}
// Dynamic dispatch for heterogeneous collection
for $item in $dynamic_items {
$item.draw();
}
}
Cost Comparison
| Approach | Heap Alloc | Vtable | Inlining | Binary Size |
|---|---|---|---|---|
| Generics | No | No | Yes | Larger (monomorphized) |
| Enums | No | No | Yes | Small |
dyn |
Yes | Yes | No | Small |
Generics and enums are zero-cost. The compiler optimizes them to direct calls. Use them by default.
dyn has runtime costs. Each call pays 2-3ns for vtable lookup, plus heap allocation overhead. Only use when you need true runtime polymorphism.
Best Practices
Start with generics or enums. These are your defaults. They're fast and the compiler checks everything at compile time.
dyn is opt-in. Only use trait objects when you genuinely need runtime polymorphism—plugins, FFI, or user-defined extensions.
Use enums for closed sets. If you know all variants upfront, enums give you exhaustive matching and zero overhead.
Be aware of object safety. If you might need trait objects, design your traits to be object-safe from the start.
Ask yourself: "Could I use an enum or generic here?" If yes, do that instead of dyn.
// Good: clear purpose for each approach
type ShapeRenderer<S: Shape> { // Generic for max performance
shape S
}
type Canvas { // Trait object for flexibility
shapes [dyn Drawable]
}
enum PrimitiveShape { // Enum for closed set
Point, Line, Triangle
}