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.

Copyright (c) 2025 Ocean Softworks, Sharkk