Generics: Type Constraints
A type constraint on a generic type parameter indicates a requirement that a type must fulfill in order to be accepted as a type argument for that type parameter. (For example, it might have to be a given class type or a subtype of that class type, or it might have to implement a given interface.)
A constraint can have one of three forms:
T as sometype
, meaning thatT
must be a subtype ofsometype
T super sometype
, meaning thatT
must be a supertype ofsometype
T = sometype
, meaning thatT
must be equivalent tosometype
. (This is like saying bothT as sometype
andT super sometype
.)
Consider the following example in which function max_val
has one type parameter, T
, and that has a constraint, num
:
function max_val<T as num>(T $p1, T $p2): T {
return $p1 > $p2 ? $p1 : $p2;
}
<<__EntryPoint>>
function main(): void {
echo "max_val(10, 20) = ".max_val(10, 20)."\n";
echo "max_val(15.6, -20.78) = ".max_val(15.6, -20.78)."\n";
}
Without the num
constraint, the expression $p1 > $p2
is ill-formed, as a greater-than operator is not defined for all types. By
constraining the type of T
to num
, we limit T
to being an int
or float
, both of which do have that operator defined.
Unlike an as
constraint, T super U
asserts that T
must be a supertype of U
. This kind of constraint is rather exotic, but solves
an interesting problem encountered when multiple types "collide". Here is an example of how it's used on method concat
in the library interface
type ConstVector
:
interface ConstVector<+T> {
public function concat<Tu super T>(ConstVector<Tu> $x): ConstVector<Tu>;
// ...
}
Consider the case in which we call concat
to concatenate a Vector<float>
and a Vector<int>
. As these have a common supertype, num
,
the super
constraint allows the checker to determine that num
is the inferred type of Tu
.
Now, while a type parameter on a class can be annotated to require that it is a subtype or supertype of a particular type, for generic parameters on classes, constraints on the type parameters can be assumed in any method in the class. But sometimes some methods want to use some features of the type parameter, and others want to use some different features, and not all instances of the class will satisfy all constraints. This can be done by specifying constraints that are local to particular methods. For example:
class MyWidget<Telem> {
public function showIt(): void where Telem as IPrintable { ... }
public function countIt(): int where Telem as ICountable { ... }
}
Constraints can make use of the type parameter itself. They can also make use of generic type parameters on the method. For example:
class MyList<T> {
public function flatten<Tu>(): MyList<Tu> where T = MyList<Tu> {
throw new Exception('unimplemented');
}
}
Here we might create a list of lists of int, of type MyList<MyList<int>>
, and then invoke flatten
on it to get a MyList<int>
. Here's another example:
class MyList<T> {
public function compact<Tu>(): MyList<Tu> where T = ?Tu {
throw new Exception('unimplemented');
}
}
A where
constraint permits multiple constraints supported; just separate the constraints with commas. For example:
class SomeClass<T> {
function foo(T $x) where T as MyInterface, T as MyOtherInterface
}
If a method overrides another method that has declared where
constraints, it's necessary to redeclare
those constraints, but only if they are actually used by the overriding method. (It's valid, and reasonable, to require less of the overriding method.)