PHP: Hypertext Preprocessor (original) (raw)

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

`<?phpabstract class Animal
{
protected string $name;

public function

__construct(string $name)
{ this−>name=this->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.

`<?phpinterface 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.

`<?phpclass Food {}

class

AnimalFood extends Food {}

abstract class

Animal
{
protected string $name;

public function

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

public function

eat(AnimalFood $food)
{
echo this−>name."eats".getclass(this->name . " eats " . get_class(this>name."eats".getclass(food);
}
}`

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

<?phpclass Dog extends Animal { public function eat(Food $food) { echo <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>t</mi><mi>h</mi><mi>i</mi><mi>s</mi><mo>−</mo><mo>&gt;</mo><mi>n</mi><mi>a</mi><mi>m</mi><mi>e</mi><mi mathvariant="normal">.</mi><mi mathvariant="normal">&quot;</mi><mi>e</mi><mi>a</mi><mi>t</mi><mi>s</mi><mi mathvariant="normal">&quot;</mi><mi mathvariant="normal">.</mi><mi>g</mi><mi>e</mi><msub><mi>t</mi><mi>c</mi></msub><mi>l</mi><mi>a</mi><mi>s</mi><mi>s</mi><mo stretchy="false">(</mo></mrow><annotation encoding="application/x-tex">this-&gt;name . &quot; eats &quot; . get_class(</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">t</span><span class="mord mathnormal">hi</span><span class="mord mathnormal">s</span><span class="mord">−</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">&gt;</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord mathnormal">nam</span><span class="mord mathnormal">e</span><span class="mord">.&quot;</span><span class="mord mathnormal">e</span><span class="mord mathnormal">a</span><span class="mord mathnormal">t</span><span class="mord mathnormal">s</span><span class="mord">&quot;.</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal">e</span><span class="mord"><span class="mord mathnormal">t</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mord mathnormal">a</span><span class="mord mathnormal">ss</span><span class="mopen">(</span></span></span></span>food); } }

The next example will show the behavior of contravariance.

`<?php

$kitty

= (new CatShelter)->adopt("Ricky");
$catFood = new AnimalFood(); kitty−>eat(kitty->eat(kitty>eat(catFood);
echo "\n";$doggy = (new DogShelter)->adopt("Mavrick");
$banana = new Food(); doggy−>eat(doggy->eat(doggy>eat(banana);`

The above example will output:

Ricky eats AnimalFood Mavrick eats Food

But what happens if kittytriesto∗∗eat()∗∗thekitty tries to eat() thekittytriestoeat()thebanana?

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

Property variance

By default, properties are neither covariant nor contravariant, hence invariant. That is, their type may not change in a child class at all. The reason for that is "get" operations must be covariant, and "set" operations must be contravariant. The only way for a property to satisfy both requirements is to be invariant.

As of PHP 8.4.0, with the addition of abstract properties (on an interface or abstract class) andvirtual properties, it is possible to declare a property that has only a get or set operation. As a result, abstract properties or virtual properties that have only a "get" operation required may be covariant. Similarly, an abstract property or virtual property that has only a "set" operation required may be contravariant.

Once a property has both a get and set operation, however, it is no longer covariant or contravariant for further extension. That is, it is now invariant.

Example #1 Property type variance

`<?php
class Animal {}
class Dog extends Animal {}
class Poodle extends Dog {}

interface

PetOwner
{
// Only a get operation is required, so this may be covariant.
public Animal $pet { get; }
}

class

DogOwner implements PetOwner
{
// This may be a more restrictive type since the "get" side
// still returns an Animal. However, as a native property
// children of this class may not change the type anymore.
public Dog $pet;
}

class

PoodleOwner extends DogOwner
{
// This is NOT ALLOWED, because DogOwner::$pet has both
// get and set operations defined and required.
public Poodle $pet;
}
?>`

Found A Problem?

xedin dot unknown at gmail dot com

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

``

Hayley Watson

2 years ago

`The gist of how the Liskov Substition Princple applies to class types is, basically: "If an object is an instance of something, it should be possible to use it wherever an instance of something is allowed". The Co- and Contravariance rules come from this expectation when you remember that "something" could be a parent class of the object.

For the Cat/Animal example of the text, Cats are Animals, so it should be possible for Cats to go anywhere Animals can go. The variance rules formalise this.

Covariance: A subclass can override a method in the parent class with one that has a narrower return type. (Return values can be more specific in more specific subclasses; they "vary in the same direction", hence "covariant").
If an object has a method you expect to produce Animals, you should be able to replace it with an object where that method produces only Cats. You'll only get Cats from it but Cats are Animals, which are what you expected from the object.

Contravariance: A subclass can override a method in the parent class with one that has a parameter with a wider type. (Parameters can be less specific in more specific subclasses; they "vary in the opposite direction", hence "contravariant").
If an object has a method you expect to take Cats, you should be able to replace it with an object where that method takes any sort of Animal. You'll only be giving it Cats but Cats are Animals, which are what the object expected from you.

So, if your code is working with an object of a certain class, and it's given an instance of a subclass to work with, it shouldn't cause any trouble:
It might accept any sort of Animal where you're only giving it Cats, or it might only return Cats when you're happy to receive any sort of Animal, but LSP says "so what? Cats are Animals so you should both be satisfied."

`

Anonymous

5 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 '

';
var_dump($a->y());
echo '
';

`