PHP quiz #6 - covariance
June 16, 2019A complete guide to covariance and contravariance in PHP 7.1.
Question
Will this code throw a warning (PHP 7.1)?
<?php
class ClassA {
public function method(array $a) {}
}
class ClassB extends ClassA {
public function method(iterable $i) {}
}
- A Yes
-
B
No
Answer
Show the answerThere are a few cases where functions can be overridden with certain signatures.
Iterable
iterable
is a pseudo-type introduced in PHP 7.1.
It's like an abstract base class for variables that can be iterated with foreach (arrays, Traversable objects).
Variance (recap)
When a subclass overrides a method of a superclass, it is possible to change its parameter and return types.
Covariance | Contravariance | Invariance |
---|---|---|
Covariance
Covariance means overriding methods can return more specific types.
An array
is more specific than an iterable
.
<?php
class ClassA {
public function method(): iterable {
return [];
}
}
class ClassB extends ClassA {
public function method(): array {
return [];
}
}
Note that this is type safe. I like to think about the following pseudocode to justify why it works:
ClassA obj = new ClassB();
iterable retval = obj.method();
Polymorphism allows us to use a subclass's (ClassB) instance anywhere where a superclass's (ClassA) instance is expected. When a subclass overrides a method, it must respect the type constraints set by its parent.
In other words, the overriding method must return an iterable. Since an array is an iterable, this requirement is satisfied.
Contravariance
Contravariance means overriding methods can accept less specific parameters.
An iterable
is less specific than an array
.
<?php
class ClassA {
public function method(array $a) {}
}
class ClassB extends ClassA {
public function method(iterable $i) {}
}
This is type safe too. It's a bit harder to reason about, but here's my try:
ClassA obj = new ClassB();
obj.method(new array());
Polymorphism allows us to use a subclass's (ClassB) instance anywhere where a superclass's (ClassA) instance is expected. When a subclass overrides a method, it must respect the type constraints set by its parent.
In this case, the overriding method must be able to receive an array type parameter. Accepting all iterables (including arrays) satisfies this requirement.
Invariance
Invariance means the overriding method cannot change the types.
Covariance, contravariance and PHP
PHP does not support covariance/contravariance. It's an invariant language. However, as always, there are a few exceptions:
1. iterable
By now it shouldn't come as a surprise that iterable
is indeed covariant/contravariant.
<?php
class ClassA {
public function method(array $a): iterable {
return [];
}
}
class ClassB extends ClassA {
public function method(iterable $i): array {
return [];
}
}
2. parameter type widening
From PHP 7.2 you can omit parameter types in overriding methods. This is an example of contravariance.
<?php
class ClassA {
public function method(Exception $e) {}
}
class ClassB extends ClassA {
public function method($e) {}
}
3. return types
If the parent method doesn’t have a return type, it can be specified. This is an example of covariance.
<?php
class ClassA {
public function method() {}
}
class ClassB extends ClassA {
public function method(): void {}
}
The future
PHP is mostly invariant, for now. But there is progress!
PHP 7.4 is going to add support for real covariance/contravariance (rfc). It will be a great improvement to PHP's type system.
This post is part of a series based on a presentation I gave on March 20, 2019.