whateverthing.com

Contra-who? Covari-wha?

With PHP's recent focus on stricter type handling comes some new terminology that might be intimidating to some and confusing to others.

Two new terms, "contravariant parameters" and "covariant returns", describe how derived classes and interface implementations can widen and narrow the declared types of their parent interface or class.

Back in the code-slinging, devil-may-care olden days of PHP (up to version 4), we didn't have to worry about this stuff, because the language's loosey-goosey typing "solved" it for us. We could come up with any horrendously tangled mess we wanted to.

But now, PHP is growing up. It has Opinions™. And those opinions mean that more care needs to be taken when smashing rocks together to craft our classes.

Introducing our Example

Say we have some Company classes and an interface that look like this:

<?php
declare(strict_types=1);

class Company {}
class MotorboatInsuranceCo extends Company {}

interface InsuredBoat {
    public function setInsurer(MotorboatInsuranceCo $company): void;
    public function getInsurer(): MotorboatInsuranceCo;
}

Now we want to create a concrete implementation:

<?php
declare(strict_types=1);

class Boat implements InsuredBoat {
    public $insurer;
    public function setInsurer(MotorboatInsuranceCo $company): void {
        $this->insurer = $company;
    }
    public function getInsurer(): MotorboatInsuranceCo {
        return $this->insurer;
    }
}

In the beginning .... Invariant Types

All the way up to PHP 7.3, only "invariant" types were supported. If we created a Sailboat class that extended our Boat class or implemented the InsuredBoat interface, the type declarations on its setInsurer() and getInsurer() methods would have to be a 100% match to the parent definition.

However, beginning in PHP 7.4, we have another way to approach things.

Contravariant Parameters

Drawing from our example, what if we want to make a specific Boat implementation for Sailboats, and we want to pass in some kind of HappySailboatInsuranceLtd object?

Our first instinct might be to rewrite the class like this:

<?php
declare(strict_types=1);

class HappySailboatInsuranceLtd extends MotorboatInsuranceCo {}

class Sailboat extends Boat {
    // ...

    // this example does not work (see below for more info)
    public function setInsurer(HappySailboatInsuranceLtd $company): void {
        $this->insurer = $company;
    }

    // ...
}

So now, when we call setInsurer() on one of these Sailboat classes, we can only pass in a HappySailboatInsuranceLtd. Right?

Actually, no. PHP won't let us declare this change from the InsuredBoat Interface and the Boat class, because it improperly narrows the accepted parameters. An object of Sailboat that implements InsuredBoat cannot accept a narrower input parameter than MotorboatInsuranceCo. It breaks the interface's contract.

Fatal error: Declaration of Sailboat::setInsurer(HappySailboatInsuranceLtd $company): void must be compatible with Boat::setInsurer(MotorboatInsuranceCo $company): void in /tmp/example2.php on line 28

Contravariant Parameters mean that the parameters can only "go wider"; that is, if MotorboatInsuranceCo is the declared type, only a parent class or interface of Company would be accepted; sub-types or sub-interfaces, like our hypothetical HappySailboatInsuranceLtd, would be rejected.

A working example would look more like this:

<?php
declare(strict_types=1);

class HappySailboatInsuranceLtd extends Company {}

class Sailboat implements InsuredBoat {
    public $insurer;

    public function setInsurer(Company $company): void {
        if (!$company instanceof HappySailboatInsuranceLtd) {
            throw new \Exception('avast me hearties');
        }

        $this->insurer = $company;
    }

    public function getInsurer(): MotorboatInsuranceCo {
        return $this->insurer;
    }
}

This wider parameter (Company $company) follows the contract of the interface and everything is copasetic ... er, happy.

Well, almost. The return type on getInsurer() is not going to be happy, not one bit.

Covariant Returns

Now we come to the other confusing bit of terminology. Let's say we want to change the return type of the getInsurance() method to make more sense in our new Sailboat class:

<?php
declare(strict_types=1);

class HappySailboatInsuranceLtd extends Company {}

class Sailboat implements InsuredBoat {
    public $insurer;

    public function setInsurer(Company $company): void {
        if (!$company instanceof HappySailboatInsuranceLtd) {
            throw new \Exception('avast me hearties');
        }

        $this->insurer = $company;
    }

    public function getInsurer(): HappySailboatInsuranceLtd {
        return $this->insurer;
    }
}

Would this be OK? Actually, again, no. Because HappySailboatInsuranceLtd extends Company and not MotorboatInsuranceCo, PHP views it as an inappropriate widening of the return type.

Fatal error: Declaration of Sailboat::getInsurer(): HappySailboatInsuranceLtd must be compatible with InsuredBoat::getInsurer(): MotorboatInsuranceCo in /tmp/example4.php on line 35

Here we have two or three options for how to solve the issue.

Option 1: We could modify the original interface to say that getInsurer() returns a Company. Then, we could modify the Boat->getInsurer() method's return type to return MotorboatInsuranceCo, and allow the Sailboat->getInsurer() method to return HappySailboatInsuranceLtd.

Option 2: We could modify the HappySailboatInsuranceLtd class to extend from the MotorboatInsuranceCo class. This is less than ideal in some architectures, because the resulting hierarchy might be misleading.

Option 3: We could create a semi-generic InsuranceCompany class that both HappySailboatInsuranceLtd and MotorboatInsuranceCo extend.

Options 1 and 3 seem the most likely to achieve our goal, so let's see what that implementation of Option 3 would look like:

<?php
declare(strict_types=1);

class Company {}
class InsuranceCompany extends Company {}
class MotorboatInsuranceCo extends InsuranceCompany {}
class HappySailboatInsuranceLtd extends InsuranceCompany {}

interface InsuredBoat {
    public function setInsurer(MotorboatInsuranceCo $company): void;
    public function getInsurer(): InsuranceCompany;
}

class Boat implements InsuredBoat {
    public $insurer;
    public function setInsurer(MotorboatInsuranceCo $company): void {
        $this->insurer = $company;
    }
    public function getInsurer(): MotorboatInsuranceCo {
        return $this->insurer;
    }
}

class Sailboat implements InsuredBoat {
    public $insurer;

    // this definition widens the parameter using contravariance
    public function setInsurer(InsuranceCompany $company): void {
        if (!$company instanceof HappySailboatInsuranceLtd) {
            throw new \Exception('avast me hearties');
        }

        $this->insurer = $company;
    }

    // this definition narrows the return type using covariance
    public function getInsurer(): HappySailboatInsuranceLtd {
        return $this->insurer;
    }
}

Looking at this, there seems to be an indication that the setInsurer method on the InsuredBoat interface could have its $company parameter type be widened to InsuranceCompany, but that's kind of just a quirk of the example.

What might be handy is a memorable way to think of the subject. There are all kinds of examples that could be drawn from, such as plumbing, the digestive system, London fatbergs, rivers, TARDISes, and so on.

Since I'm rather obsessed with boats, I feel like charting a nautical tack.

For "Contravariant Parameters", think of arriving at a marina in your boat. If it's a regular powerboat, you can take it to your boathouse and it'll fit right in. But if you happened to swap that boat out with a sailboat while you are motoring around (you piratey sea-dog, you!), and you try to put that sailboat in your boathouse, it's not going to fit. While you'll be able to find a berth for it in the open part of the marina, the boathouse's roof blocks the ship's mast from entering. That's a covariant parameter: it can go wider (going somewhere else in the marina), but it can't go narrower (fitting a sailboat into a boathouse).

For "Covariant Returns", consider a sailboat in a storm. In calm weather, the mainsail can be fully up. But when the wind hits, you "reef" it down. There are set reefing points that you can tie off in order to reduce the amount of sail area and sail more safely. It can go narrower (second reef, third reef), but as long as the wind is howling, it can't (or shouldn't, for safety) go wider.

Let's Get Real

Okay, maybe my contrived examples aren't the best way of conveying the idea. Luckily, there is a real-world use case of these concepts that we can look to.

The PHP Framework Interoperability Group (FIG) has released a number of standardized interfaces over the years. As the language has evolved, the need has grown to revisit those interfaces and allow them to adapt as well.

Breaking backwards compatibility is frowned on, especially for critical interoperability interfaces. So they needed a plan to upgrade the interfaces that wouldn't break the world out of the gate.

What they came up with was a two-step dance of "Version 2" and "Version 3". In this plan, "Version 3" would be the final or ideal form of the new interface, and "Version 2" would be an intermediate step that introduces part of the upgrade in a way that allows older non-updated libraries to continue to work.

Version 2 of a PSR will use Contravariant Parameters to bring an element of typedness to the interface. At the same time, Version 3 of the PSR will be released, which follows on to fill in the blanks by adding Covariant Returns.

Any projects that code against the original Version 1 of the PSR would automatically be compatible with Version 2, because their methods would have "wider" (i.e., untyped) parameters. This is where Contravariants shine.

Then, the implementing library/project can release an updated version that is compatible with both Version 2 and Version 3. Adding return types, which uses covariance to narrow the contract, still allows Version 2 of the PSR to be used.

Libraries would also have the option of following the same two-step process, if they would prefer.

For more information about FIG's proposal, check out their blog post on upgrading PSRs.

Edging Forward

I hope this helps folks wrap their head around how and when to apply Contravariants/Covariants to their own projects. For additional reading, the PHP manual has a section that covers the different types of variance.

Let's embrace the future of PHP -- and bring our codebases along for the ride.

Thanks for reading!

Published: March 13, 2020

Categories: coding

Tags: dev, development, coding, php, maintenance

Related Posts