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

Structural Types

Whim can describe the parts of arrays, objects, and callables at runtime.

Homogeneous vecs

vec<T> accepts a vec whose every value fits T.

function total(vec<int> $values): int {
  $sum = 0;
  foreach ($values as $value) {
    $sum += $value;
  }

  return $sum;
}

The vec’s runtime element type changes as code adds or removes values. An empty vec has element type never, so it fits vec<T> for any T.

Bare vec accepts a vec without checking its items at a declared boundary. The matching forms is vec, as vec, and a bare vec match pattern are not valid; write vec<_> when an explicit runtime check may accept any items.

Vec shapes

vec[T0, T1] describes fixed positions and an exact length.

function pair(vec[int, string] $value): void {}

pair(vec[42, 'answer']);

A final ...T allows zero or more extra values of T:

type Row = vec[string, ...int];

assert!(vec['row'] is Row);
assert!(vec['row', 1, 2] is Row);

Homogeneous dicts

dict<K, V> checks every key against K and every value against V.

function scores(dict<string, int> $scores): void {}

scores(dict['Ada' => 10, 'Grace' => 12]);

Dict keys can be int, uint, string, bool, or fresh. The type may use one of them, a union, a range, or another type that fits those key kinds.

An empty dict has type dict<never, never> and fits every valid dict key and value type.

Bare dict and array follow the same boundary rule. In an explicit runtime check, use dict<_, _> or array<_, _>.

Dict shapes

A dict shape lists required keys and their value types.

Keys may be string, integer, or boolean literals. As in dict values, true, 1, and '1' are distinct keys, as are false, 0, and '0'.

type UserRow = dict['id' => int, 'name' => string];

$user = dict['id' => 1, 'name' => 'Ada'];
assert!($user is UserRow);
type Partition<T> = dict[true => vec<T>, false => vec<T>];

$partition = dict[true => vec[1, 3], false => vec[2, 4]];
assert!($partition is Partition<int>);
assert!(!(dict[1 => vec[1, 3], 0 => vec[2, 4]] is Partition<int>));

Without a rest entry, the dict must have only the listed keys. A rest entry allows other keys and gives them a key and value type:

type Named = dict['name' => string, ...];
type ScoredUser = dict['id' => int, 'name' => string, ...<string, int|float>];

$user = dict['id' => 1, 'name' => 'Ada', 'score' => 9.5];
assert!($user is ScoredUser);

Object shapes

#{ name: string } accepts an object with exactly one public instance property, name, whose current value is a string. Add a final ... to allow other public instance properties:

type NamedObject = #{ name: string, ... };
type WithValue<T> = #{ value: T, ... };

class Person {
  public string $name = 'Ada';
  public int $age = 36;
}

$person = new Person();
assert!($person is NamedObject);
assert!($person is #{ age: int, name: string });
assert!(!($person is #{ name: string }));

Property names are case-sensitive identifiers, without $. Required properties must exist, be public instance properties, be initialized, and have values that match their corresponding types. Inherited public properties count. Private, protected, and static properties do not participate, even when the check runs inside their declaring class. An uninitialized required property rejects a check; it does not throw an uninitialized-property error. An extra public property still counts toward exactness when it is uninitialized.

#{} accepts objects with no public instance properties. #{ ... } accepts any object, just like object. Neither form accepts an array or another non-object value. Object shapes have a bare ... rest marker; unlike dict shapes, they do not have a typed rest entry. Property names cannot repeat. Trailing commas are allowed.

Shapes check current values, independently of a property’s declared type. A mixed property holding an integer can satisfy an int entry. A successful check does not change the property’s declared type, visibility, or readonly rules. Mutating the object can make a later check fail, including when the object is held inside a vec, dict, or tuple.

Place a shape after a named type to combine the nominal and structural checks. Name #{ ... } is shorthand for Name & #{ ... }, including generic names:

use Whim\Result\Ok;

$value = new Ok::<string>('hello');
assert!($value is Ok<string> & #{ value: string & !'' });
assert!($value is Ok<string> #{ value: string & !'' });

The name and shape form one type: !Name #{ value: int } negates the entire combination, like !(Name & #{ value: int }). Closed and open property-set rules apply equally to both spellings.

Shapes work in aliases, generic arguments and bounds, callable signatures, parameter and return types, property types, is, as, and ?as. Their subtyping rules compare required names independently of source order and allow narrower property value types. An open expected shape allows extra properties; a closed expected shape requires the same public property set and a closed actual shape. Every object shape is a subtype of object. A class name alone does not promise that its properties are initialized with the required current values; use a shape or an intersection to express that requirement.

The opening #{ is one token. Whitespace or a comment cannot separate # from {. Object shapes are types and patterns, not object construction expressions. See Match and Destructuring for extracting their properties in a match arm.

Tuple types

(A, B) describes an exact tuple length and each position.

type Coordinate = (float, float);

function move(Coordinate $point): Coordinate {
  return ($point[0] + 1.0, $point[1] + 1.0);
}

A one-item tuple type has a trailing comma: (T,).

A final ...T accepts zero or more trailing items of T:

type Delivery = (int, int, ...string);

assert!((41, 99) is Delivery);
assert!((41, 99, 'fragile', 'signed') is Delivery);

The rest item must be last. Omitting its type, as in (int, ...), uses mixed. A tuple has at least one and at most twelve fixed items.

The common array type

array<K, V> accepts a tuple, vec, or dict whose keys fit K and values fit V.

function count_values(array<_, _> $values): int {
  return length!($values) as int;
}

assert!(count_values(vec[1, 2]) == 2);
assert!(count_values(dict['one' => 1]) == 1);
assert!(count_values((1, 'two')) == 2);

Vec and tuple keys are uint positions starting at 0u. Nonempty vecs and tuples can satisfy array<uint, V> but cannot satisfy array<int, V>. Dict keys keep their declared kinds.

array<K, V> is a read-only type view. It does not change the value into a new array form.

Callable types

fn(A, B): R describes a callable that accepts A and B and returns R.

function apply(fn(int): string $format, int $value): string {
  return $format($value);
}

$result = apply(fn(int $value): string => 'n=' . $value, 42);

A leading = marks an optional parameter in a callable type:

fn(int, =string): bool

This type accepts a callable that lets callers omit its second argument.

Bare fn accepts any callable.

Callable parameters are contravariant. A callable that accepts mixed can stand in for one that needs only int. Return types are covariant. A callable that returns int can stand in for one that may return int|string.

Callable values also carry their generic binding and origin. The symbols as types chapter covers specific function and method families.

Class-name types

classname<T> accepts a string that names a class whose instances fit T.

interface Shape {}
class Circle implements Shape {}

function make(classname<Shape> $class): Shape {
  return new $class();
}

$shape = make('Circle');

The string may include reified type arguments, such as Box<int>. A child class name also fits a parent or interface bound. A missing class or wrong type argument fails the check.

The inner type must be able to contain a class-like type. classname<int> is invalid.

Wildcards

_ means that a nested type exists but its fixed value does not matter.

assert!(vec[1, 'two'] is vec<_>);
assert!(dict['one' => 1] is dict<_, _>);

It can select some generic arguments while keeping others fixed:

final class Pair<A, B> {}

$pair = new Pair::<string, int>();
assert!($pair is Pair<_, int>);

It also works in tuple positions and callable parameters.

_ cannot stand alone as a type. Whim does not allow !_. It also does not mean mixed: Box<_> accepts a Box<string>, while Box<mixed> follows the class’s variance and may reject it.