Covariance and Contravariance

In PHP 7.2.0, partial contravariance was introduced by removing type restrictions on parameters in a child method. As of PHP 7.4.0, full covariance and contravariance support was added.

Covariance allows a child's method to return a more specific type than the return type of its parent's method. Whereas, contravariance allows a parameter type to be less specific in a child method, than that of its parent.

A type declaration is considered more specific in the following case:

A type class is considered less specific if the opposite is true.

Covariance

To illustrate how covariance works, a simple abstract parent class, Animal is created. Animal will be extended by children classes, Cat, and Dog.

<?php

abstract class Animal
{
protected
string $name;

public function
__construct(string $name)
{
$this->name = $name;
}

abstract public function
speak();
}

class
Dog extends Animal
{
public function
speak()
{
echo
$this->name . " barks";
}
}

class
Cat extends Animal
{
public function
speak()
{
echo
$this->name . " meows";
}
}

Note that there aren't any methods which return values in this example. A few factories will be added which return a new object of class type Animal, Cat, or Dog.

<?php

interface AnimalShelter
{
public function
adopt(string $name): Animal;
}

class
CatShelter implements AnimalShelter
{
public function
adopt(string $name): Cat // instead of returning class type Animal, it can return class type Cat
{
return new
Cat($name);
}
}

class
DogShelter implements AnimalShelter
{
public function
adopt(string $name): Dog // instead of returning class type Animal, it can return class type Dog
{
return new
Dog($name);
}
}

$kitty = (new CatShelter)->adopt("Ricky");
$kitty->speak();
echo
"\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$doggy->speak();

The above example will output:

Ricky meows
Mavrick barks

Contravariance

Continuing with the previous example with the classes Animal, Cat, and Dog, a class called Food and AnimalFood will be included, and a method eat(AnimalFood $food) is added to the Animal abstract class.

<?php

class Food {}

class
AnimalFood extends Food {}

abstract class
Animal
{
protected
string $name;

public function
__construct(string $name)
{
$this->name = $name;
}

public function
eat(AnimalFood $food)
{
echo
$this->name . " eats " . get_class($food);
}
}

In order to see the behavior of contravariance, the eat method is overridden in the Dog class to allow any Food type object. The Cat class remains unchanged.

<?php

class Dog extends Animal
{
public function
eat(Food $food) {
echo
$this->name . " eats " . get_class($food);
}
}

The next example will show the behavior of contravariance.

<?php

$kitty
= (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood();
$kitty->eat($catFood);
echo
"\n";

$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food();
$doggy->eat($banana);

The above example will output:

Ricky eats AnimalFood
Mavrick eats Food

But what happens if $kitty tries to eat() the $banana?

$kitty->eat($banana);

The above example will output:

Fatal error: Uncaught TypeError: Argument 1 passed to Animal::eat() must be an instance of AnimalFood, instance of Food given
add a note add a note

User Contributed Notes 4 notes

up
37
xedin dot unknown at gmail dot com
4 years ago
I would like to explain why covariance and contravariance are important, and why they apply to return types and parameter types respectively, and not the other way around.

Covariance is probably easiest to understand, and is directly related to the Liskov Substitution Principle. Using the above example, let's say that we receive an `AnimalShelter` object, and then we want to use it by invoking its `adopt()` method. We know that it returns an `Animal` object, and no matter what exactly that object is, i.e. whether it is a `Cat` or a `Dog`, we can treat them the same. Therefore, it is OK to specialize the return type: we know at least the common interface of any thing that can be returned, and we can treat all of those values in the same way.

Contravariance is slightly more complicated. It is related very much to the practicality of increasing the flexibility of a method. Using the above example again, perhaps the "base" method `eat()` accepts a specific type of food; however, a _particular_ animal may want to support a _wider range_ of food types. Maybe it, like in the above example, adds functionality to the original method that allows it to consume _any_ kind of food, not just that meant for animals. The "base" method in `Animal` already implements the functionality allowing it to consume food specialized for animals. The overriding method in the `Dog` class can check if the parameter is of type `AnimalFood`, and simply invoke `parent::eat($food)`. If the parameter is _not_ of the specialized type, it can perform additional or even completely different processing of that parameter - without breaking the original signature, because it _still_ handles the specialized type, but also more. That's why it is also related closely to the Liskov Substitution: consumers may still pass a specialized food type to the `Animal` without knowing exactly whether it is a `Cat` or `Dog`.
up
2
Anonymous
4 years ago
Covariance also works with general type-hinting, note also the interface:

interface xInterface
{
    public function y() : object;
}

abstract class x implements xInterface
{
    abstract public function y() : object;
}

class a extends x
{
    public function y() : \DateTime
    {
        return new \DateTime("now");
    }
}

$a = new a;
echo '<pre>';
var_dump($a->y());
echo '</pre>';
up
0
maxim dot kainov at gmail dot com
3 years ago
This example will not work:

<?php

class CatFood extends AnimalFood { }

class
Cat extends Animal
{
    public function
eat(CatFood $food) {
        echo
$this->name . " eats " . get_class($food);
    }
}

?>

The reason is:

<?php
   
class DogFood extends AnimalFood { }
  
    function
feedAnimal(Animal $animal, AnimalFood $food) {
       
$animal->eat($food);  
    }

   
$cat = new Cat();
   
$dogFood = new DogFood();  

   
feedAnimal($cat, $dogFood);   
?>

But you can do it with traits, like this:

<?php

trait AnimalTrait
{
    public function
eat(AnimalFood $food)
    {
        echo
$this->name . " ест " . get_class($food);
    }
}

class
Cat
{
    use
AnimalTrait;

    public function
eat(CatFood $food) {
        echo
$this->name . " eats " . get_class($food);
    }
}

?>
up
-2
phpnet-at-kennel17-dotco-dotuk
3 years ago
Following the examples above, you might assume the following would be possible.

<?php

class CatFood extends AnimalFood { ... }

class
Cat extends Animal
{
    public function
eat(CatFood $food) {
        echo
$this->name . " eats " . get_class($food);
    }
}

?>

However, the Liskov Substitution Prinicpal, and therefore PHP, forbids this.  There's no way for cats to eat cat food, if animals are defined as eating animal food.

There are a large number of legitimate abstractions that are forbidden by PHP, due to this restriction.
To Top