Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Covariance, Contravariance & Diamond

Covariance, Contravariance & Diamond

Talk given at ForumPHP 2024

Alexandre Daubois

October 14, 2024
Tweet

More Decks by Alexandre Daubois

Other Decks in Technology

Transcript

  1. WHO AM I Alexandre Daubois @alexdaubois Expert Symfony Developer Expert

    Certified Dev Top 10 contributors of Symfony Author of “Clean Code in PHP”
  2. Initial example <?php abstract class OperatingSystem { abstract public function

    createAndCopyFrom(OperatingSystem $os): OperatingSystem; } class Linux extends OperatingSystem { public function createAndCopyFrom(OperatingSystem $os): OperatingSystem { // ... } } class MacOS extends OperatingSystem { public function createAndCopyFrom(OperatingSystem $os): OperatingSystem { // ... } }
  3. <?php abstract class OperatingSystem { abstract public function createAndCopyFrom(OperatingSystem $os):

    OperatingSystem; } class Linux extends OperatingSystem { public function createAndCopyFrom(OperatingSystem $os): OperatingSystem|string { // ... } }
  4. <?php abstract class OperatingSystem { abstract public function createAndCopyFrom(OperatingSystem $os):

    OperatingSystem; } class Linux extends OperatingSystem { public function createAndCopyFrom(OperatingSystem $os): Linux { // ... } }
  5. <?php abstract class OperatingSystem { abstract public function createAndCopyFrom(OperatingSystem $os):

    OperatingSystem; } class Linux extends OperatingSystem { public function createAndCopyFrom(Linux $os): OperatingSystem { // ... } }
  6. <?php abstract class OperatingSystem { abstract public function createAndCopyFrom(OperatingSystem $os):

    OperatingSystem; } class Linux extends OperatingSystem { public function createAndCopyFrom(OperatingSystem|string $os): OperatingSystem { // ... } }
  7. Let’s recap what you can do. But also what you

    cannot. Widen argument type Yes Narrow argument type No Widen return type No Narrow return type Yes
  8. (What if cURL is missing?) <?php use Symfony\Component\HttpClient\CurlHttpClient; class OperatingSystemIsoDownloader

    { public function __construct(private CurlHttpClient $client) { } public function download(string $url) { return $this->client->request(/* ... */); } }
  9. Let f(x) be a property provable about objects x of

    type T. Then f(y) should be true for objects y of type S where S is a subtype of T. Barbara Liskov A behavioral notion of subtyping - 1994 Sales Deck Name Wikimedia commons
  10. <?php class User implements \Stringable { public function __toString(): string

    { return '...'; } } class Admin extends User {} $user = new User(); $admin = new Admin(); assert($user instanceof \Stringable); assert($admin instanceof \Stringable); // Liskov!
  11. <?php interface UserInterface {} class User { public function getReferral():

    UserInterface {} } class Admin extends User {} $user = new User(); $admin = new Admin(); assert($user->getReferral() instanceof UserInterface); assert($admin->getReferral() instanceof UserInterface); // Liskov!
  12. <?php interface UserInterface {} interface AdminInterface extends UserInterface {} class

    User { public function getReferral(): UserInterface {} } class Admin extends User { public function getReferral(): AdminInterface {} } $user = new User(); $admin = new Admin(); assert($user->getReferral() instanceof UserInterface); assert($admin->getReferral() instanceof UserInterface); // Boom, Liskov again!
  13. <?php interface UserInterface {} interface AdminInterface extends UserInterface {} class

    User { public function getReferral(): UserInterface {} } class Admin extends User { public function getReferral(): AdminInterface|string {} } $user = new User(); $admin = new Admin(); assert($user->getReferral() instanceof UserInterface); assert($admin->getReferral() instanceof UserInterface); // PHP error
  14. <?php interface UserInterface {} interface AdminInterface extends UserInterface {} class

    User { public function getReferral(): AdminInterface {} } class Admin extends User { public function getReferral(): UserInterface {} } $user = new User(); $admin = new Admin(); assert($user->getReferral() instanceof UserInterface); assert($admin->getReferral() instanceof UserInterface); // PHP error
  15. <?php function act(User $user) { $user->setIdentifier(1); } class User {

    public function setIdentifier(int $i) { /* ... */ } } class Admin extends User {} act(new User()); act(new Admin());
  16. <?php function act(User $user) { $user->setIdentifier(1); } class User {

    public function setIdentifier(int $i) { /* ... */ } } class Admin extends User { public function setIdentifier(int|string $i) { /* ... */ } } act(new User()); act(new Admin());
  17. <?php // Simplified SPL Reminder interface Traversable {} class ArrayObject

    implements Traversable {} // Back to our code function act(User $user) { $user->doSomething(new \IteratorIterator(/* ... */)); } class User { public function attachReferrals(\Traversable $refs) { /* ... */ } } class Admin extends User { public function attachReferrals(\ArrayObject $refs) { /* ... */ } } act(new User()); act(new Admin()); // Boom, PHP error!
  18. Let’s recap what you can do. But also what you

    cannot. Widen argument type Yes Narrow argument type No Widen return type No Narrow return type Yes
  19. <?php interface UserInterface {} class User implements UserInterface {} /**

    * @template T */ class TypedArray { /** * @param array<T> $elements */ public function __construct(private array $elements) {} /** * @return T */ public function at(int $index): mixed { return $this->elements[$index]; } } /** @var TypedArray<UserInterface> $array */ $array = new TypedArray([new User(), new User()]); $user = $array->at(0); // IDEs and static analyzers should know that $user is of type UserInterface
  20. <?php interface UserInterface {} class User implements UserInterface {} /**

    * @template T */ class TypedArray { /** * @param array<T> $elements */ public function __construct(private array $elements) {} /** * @return T */ public function at(int $index): mixed { return $this->elements[$index]; } } /** @var TypedArray<UserInterface> $array */ $array = new TypedArray([new User(), new User()]); $user = $array->at(0); // IDEs and static analyzers should know that $user is of type UserInterface
  21. <?php interface UserInterface {} class User implements UserInterface {} /**

    * @template T */ class TypedArray { /** * @param array<T> $elements */ public function __construct(private array $elements) {} /** * @return T */ public function at(int $index): mixed { return $this->elements[$index]; } } /** @var TypedArray<UserInterface> $array */ $array = new TypedArray([new User(), new User()]); $user = $array->at(0); // IDEs and static analyzers should know that $user is of type UserInterface
  22. <?php class MyDynamicClass extends \stdClass {} /** * @template-covariant T

    of \stdClass */ class TypedArrayOfDynamicObjects { // ... /** * @param T value */ public function push(mixed $value): void { // ... } } /** @var TypedArrayOfDynamicObjects<MyDynamicClass> $array */ $array = new TypedArrayOfDynamicObjects([ new MyDynamicClass(), new MyDynamicClass(), ]); $array->push(new \stdClass()); // IDEs and static analyzers should warn here
  23. <?php class MyDynamicClass extends \stdClass {} /** * @template-covariant T

    of \stdClass */ class TypedArrayOfDynamicObjects { // ... /** * @param T value */ public function push(mixed $value): void { // ... } } /** @var TypedArrayOfDynamicObjects<MyDynamicClass> $array */ $array = new TypedArrayOfDynamicObjects([ new MyDynamicClass(), new MyDynamicClass(), ]); $array->push(new \stdClass()); // IDEs and static analyzers should warn here
  24. <?php class MyDynamicClass extends \stdClass {} /** * @template-covariant T

    of \stdClass */ class TypedArrayOfDynamicObjects { // ... /** * @param T value */ public function push(mixed $value): void { // ... } } /** @var TypedArrayOfDynamicObjects<MyDynamicClass> $array */ $array = new TypedArrayOfDynamicObjects([ new MyDynamicClass(), new MyDynamicClass(), ]); $array->push(new \stdClass()); // IDEs and static analyzers should warn here
  25. <?php class TypedArrayOfDynamicObjects { // ... /** * @param TypedArrayOfDynamicObjects<covariant

    \stdClass> $array */ public function merge(TypedArrayOfDynamicObjects $array): static { // ... } }
  26. <?php interface Button { public function render(): void; } interface

    Link { public function render(): void; } class ButtonLink implements Button, Link { public function render(): void { echo '<button>Click me</button>'; } }
  27. <?php interface UIElement { public function render(); } class Button

    implements UIElement { public function render(): void { echo '<button>Click me</button>'; } } class Link implements UIElement { public function render(): void { echo '<a href="#">Click me</a>'; } } class ButtonLink extends Button, Link {}
  28. Which implementation should be used? <?php interface UIElement { public

    function render(); } class Button implements UIElement { public function render(): void { echo '<button>Click me</button>'; } } class Link implements UIElement { public function render(): void { echo '<a href="#">Click me</a>'; } } class ButtonLink extends Button, Link {}
  29. Must use full “namespace” Crashes otherwise C++ Scala Priority algorithm

    Python C3 linearization What about other languages?
  30. <?php trait Button { public function render(): void { echo

    '<button>Click me</button>'; } } trait Link { public function render(): void { echo '<a href="#">Click me</a>'; } } class ButtonLink { use Button, Link; // But fatal error! }
  31. <?php trait Button { public function render(): void { /*

    ... */ } } trait Link { public function render(): void { /* ... */ } } class ButtonLink { use Button, Link { Button::render insteadof Link; Button::render as renderButton; Link::render as renderLink; } public function renderElement(): void { $this->renderButton(); // calls Button::render $this->renderLink(); // calls Link::render } }
  32. <?php trait Button { public function render(): void { /*

    ... */ } } trait Link { public function render(): void { /* ... */ } } class ButtonLink { use Button, Link { Button::render insteadof Link; Button::render as renderButton; Link::render as renderLink; } public function renderElement(): void { $this->renderButton(); // calls Button::render $this->renderLink(); // calls Link::render } }
  33. <?php trait Button { public function render(): void { /*

    ... */ } } trait Link { public function render(): void { /* ... */ } } class ButtonLink { use Button, Link { Button::render insteadof Link; Button::render as renderButton; Link::render as renderLink; } public function renderElement(): void { $this->renderButton(); // calls Button::render $this->renderLink(); // calls Link::render } }