Template parameters in depth
The universe chapter on templates covered the template-parameter Element kind. This chapter covers what happens with template parameters: how a class's parameters are declared, what the analyser must register about them, and how the lattice consults that information.
Declaration
A class with template parameters is declared in PHP via PHPDoc:
/**
* @template T
* @template-covariant V
* @template K of array-key
* @template U of Iterator = ArrayIterator
*/
class Box {
// ...
}
The analyser parses each @template line and registers, for each parameter:
- A name (
T,V,K,U). - A defining entity (the class
Box). - An upper bound (the
of Xclause; defaults tomixed). - A variance (covariant, contravariant, invariant; defaults to invariant unless declared otherwise).
- An optional default (the
= Xclause; used when the user supplies fewer arguments than declared).
Suffete itself does not store this information. The analyser registers it with its world implementation, and the lattice queries the world when it needs a parameter's variance, upper bound, or default.
What the world supplies
The world tells the lattice three things about a class's template parameters:
- The arity (how many
@templatelines the class declares). - For each position, the parameter's variance, upper bound, and default.
- For each (descendant, ancestor, position), the type the descendant supplies to the ancestor's parameter at that position.
The third one is what makes inheritance work: see specialise.
Instantiation
A use-site instantiation Box<int, string> is a named-object Element that carries its type arguments in declaration order. The first argument fills T, the second fills V. If the class declares more parameters than supplied, the missing ones are filled from the upper bound (or the declared default) and the type is flagged as having received a template default ; the lattice tolerates the default at variance check time.
Inheritance and parameter mapping
When class Bag<X> extends Box<X, int>:
Bag'sXcorresponds toBox'sT.Box'sVis bound tointfromBag's perspective.
The lattice uses this when checking Bag<string> refines Box<string, int>: it asks the world what Bag supplies to Box's parameters, substitutes Bag's actual arguments through, and compares positionally with Box's declared variance.
The full algorithm is in specialise.
Defining entities
Every template parameter is keyed by (name, defining_entity). Two parameters with the same name on the same class are the same parameter ; two with the same name on different classes are different parameters. The defining entity can be:
- A class-like (the parameter is declared on a class, interface, trait, or enum).
- A function or method.
- A closure (analyser-assigned identity).
Capture-free substitution uses the defining entity to know which parameters a substitution applies to.
Free vs bound vs partially-applied
A template-parameter Element is free until the analyser substitutes it. Three states:
- Free. The parameter appears in the type with no commitment to a value.
Box<T>::valueisT(free). The lattice can answer questions aboutTusing its constraint as an upper bound. - Bound. The parameter has been substituted. After
Box<int>::value,T := int, the field type isint, noTElement remains. - Partially applied / default-filled. The user wrote
Boxinstead ofBox<int>. The analyser fillsTwith the upper bound (mixedby default), flags the type as carrying a template default, and the lattice tolerates the default at variance check time (recording a coercion cause).
A worked example
/**
* @template T
* @template-covariant V
*/
class Map {
/** @var array<T, V> */
public array $entries = [];
/**
* @param T $key
* @return V
*/
public function get($key) { return $this->entries[$key]; }
}
/**
* @extends Map<string, mixed>
*/
class StringMap extends Map {
/** @var array<string, mixed> */
public array $entries = [];
}
Inside Map's body, the field $entries has type array<T, V>, where T and V are template-parameter Elements with defining entity Map.
Inside StringMap's body, the field $entries has type array<string, mixed> ; fully concrete because StringMap extends Map<string, mixed>.
When the analyser checks StringMap refines Map<string, mixed>:
- The lattice gets the container's parameters:
Map'sT(invariant by default) andV(covariant, declared with@template-covariant). - It asks the world what
StringMapsupplies toMap's position 0 ; the answer isstring. - It asks the same for position 1 ; the answer is
mixed. - Compare position 0 with invariant:
stringis equivalent tostring. ✓ - Compare position 1 with covariant:
mixedrefinesmixed. ✓ - Result:
StringMaprefinesMap<string, mixed>. ✓
The variance, the inheritance binding, and the per-position check are all driven by the world. Suffete itself orchestrates the dispatch.
See also: variance for the per-variance refinement rules; substitute for how
Tis replaced with a concrete type; standin for inferringTfrom call-site arguments; specialise for the inheritance-binding resolution.