Bendegúz Csirmaz

PHP quiz #6 - covariance

A 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 answer
B No

There are a few cases where functions can be overridden with certain signatures.

Iterable

iterable is a pseudo-type introduced in PHP 7.1.

iterable's inheritance tree

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 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.