Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generics

Whim generics keep their type arguments at runtime. Classes, interfaces, functions, methods, closures, aliases, and newtypes may declare type parameters.

final class Box<T> {
  public function __construct(public T $value) {}
}

function identity<T>(T $value): T {
  return $value;
}

$box = new Box::<int>(42);
$value = identity::<string>('Whim');

Declaring type parameters

Type parameters appear between < and > after a symbol name.

final class Pair<A, B> {
  public function __construct(public A $first, public B $second) {}
}

Each name must be unique in that list. A nested generic declaration may add its own parameters:

final class Box<T> {
  public function __construct(public T $value) {}

  public function map<U>(fn(T): U $transform): Box<U> {
    return new Box::<U>($transform($this->value));
  }
}

Supplying type arguments

A type annotation uses ordinary angle brackets:

Box<int>
dict<string, Box<int>>

A constructor or call uses ::<...>:

final class Box<T> {
  public function __construct(public T $value) {}
}

function identity<T>(T $value): T {
  return $value;
}

$box = new Box::<int>(1);
$value = identity::<int>(2);

The number of arguments must match the number of parameters that lack defaults. Whim does not infer a missing type argument from a value. Supply it or declare a default.

This applies to functions, methods, static methods, closures, and arrows.

$identity = fn<T>(T $value): T {
  return $value;
};

$value = $identity::<string>('text');

Runtime reification

The type argument remains available during the call and in the object.

function matches<T>(mixed $value): bool {
  return $value is T;
}

assert!(matches::<vec<int>>(vec[1, 2]));
assert!(!matches::<vec<int>>(vec[1, 'two']));

An object test includes its type arguments:

final class Box<T> {}

$box = new Box::<int>();
assert!($box is Box<int>);
assert!(!($box is Box<string>));

A generic class may use its parameter in properties, methods, parent types, and implemented interfaces. Every write and call keeps that binding.

Defaults

= Type gives a type parameter a default.

final class Box<T = int> {}

function identity<T = int>(T $value): T {
  return $value;
}

$box = new Box();
$value = identity(42);

Whim applies the default when the caller omits the argument. The default must meet the parameter’s bound. A default cannot depend on its own parameter before that parameter has a binding.

Defaults also let a generic callable fit a non-generic callable type. A generic callable with no defaults still needs type arguments when invoked.

Bounds

T: Bound limits the type arguments accepted for T.

interface Named {
  public function name(): string;
}

function label<T: Named>(T $value): string {
  return $value->name();
}

Whim checks each supplied or defaulted type argument against the bound. Bounds can use any type expression, including unions, ranges, negation, constants, and other type parameters.

Use + when one parameter must meet several bounds:

interface Named {}
interface Stored {}

function save<T: Named + Stored>(T $value): void {}

A bound may depend on another parameter:

type Weaken<T: W, W> = W;

Here T must fit W, and the alias exposes only W at runtime.

Constructing a type parameter

Code may create a reified type parameter when its bound supplies a constructor.

interface Constructable {
  public function __construct();
}

function create<T: Constructable>(): T {
  return new T();
}

A bound may also supply static methods:

interface Buildable {
  public static function build(int $seed): static;
}

function build<T: Buildable>(int $seed): T {
  return T::build($seed);
}

Calling a method through a type parameter with no matching bound fails. Whim does not allow class-constant access through a type parameter.

Variance

Variance controls how one generic type relates to another.

Covariance

out T marks an output type parameter.

interface Source<out T> {
  public function read(): T;
}

If int fits int|string, then Source<int> fits Source<int|string>.

A covariant parameter may appear in return types and readonly properties. It may not appear where the caller can send a value in, such as a writable property or method parameter.

Contravariance

in T marks an input type parameter.

interface Sink<in T> {
  public function write(T $value): void;
}

If int fits mixed, then Sink<mixed> fits Sink<int>.

A contravariant parameter may appear in method parameters. It may not appear as an output type.

Invariance

An unmarked parameter is invariant. Cell<int> and Cell<int|string> are then different types, and neither fits the other only because their arguments do.

final class Cell<T> {
  public function __construct(public T $value) {}
}

Whim checks variance in aliases, classes, interfaces, functions, methods, closures, and arrows. A negation reverses the position while Whim checks it.

Generic inheritance

A child binds its parent’s parameters in its extends or implements clause.

interface Source<out T> {
  public function read(): T;
}

abstract class Base<T> {
  public function __construct(protected T $value) {}
}

final class IntegerSource extends Base<int> implements Source<int> {
  public function read(): int {
    return $this->value;
  }
}

The object is an IntegerSource, a Base<int>, and a Source<int>.

Whim rejects a class that reaches the same generic interface with incompatible type arguments.

Generic first-class callables

A first-class callable may remain unbound:

function identity<T>(T $value): T {
  return $value;
}

$generic = identity(...);
$value = $generic::<int>(42);

Calling $generic(42) fails because T has no binding or default.

Bind the type argument while creating the callable when all calls should use one type:

function identity<T>(T $value): T {
  return $value;
}

$integers = identity::<int>(...);
assert!($integers(42) == 42);

A bound callable keeps that type and rejects another binding.

Empty types and covariance

never is useful for variant types. A value such as Result<int, never> can fit Result<int, string> when the error parameter is covariant: it cannot hold an error, so the wider error type is safe.

The same rule explains why an empty vec<never> fits vec<int>.