공변성과 반공변성

공변성과 반공변성 (Covariance and Contravariance)

PHP의 타입 시스템에서 상속 관계에 있는 메서드의 반환 타입(공변)과 파라미터 타입(반공변)이 어떻게 유연해졌는지를 다룬다. PHP 7.4 이후 본격 지원된 이 개념과 프로퍼티 타입의 변화 규칙을 예제로 설명한다.

출처: Covariance and Contravariance

본문

PHP 7.2.0 에서 하위(자식) 메서드의 파라미터에 대한 타입 제한을 제거함으로써 부분적인 반공변(partial contravariance)이 도입되었습니다. PHP 7.4.0 부터는 완전한 공변성과 반공변성을 지원합니다.

공변성(covariance) 은 자식 메서드가 부모 메서드의 반환 타입보다 더 구체적인(specific) 타입을 반환할 수 있게 해 줍니다. 반공변성(contravariance) 은 자식 메서드의 파라미터 타입이 부모의 그것보다 덜 구체적일 수 있게 해 줍니다.

타입 선언이 더 구체적인 것으로 간주되는 경우는 다음과 같습니다.

  • 유니언 타입(union type)에서 타입 하나가 제거된 경우
  • 인터섹션 타입(intersection type)에 타입 하나가 추가된 경우
  • 클래스 타입이 자식 클래스 타입으로 바뀐 경우
  • iterablearray 또는 Traversable 로 바뀐 경우

반대의 경우 타입 선언은 덜 구체적인 것으로 간주됩니다.

공변성 (Covariance)

공변성이 어떻게 동작하는지 보여 주기 위해, 간단한 추상 부모 클래스 Animal 을 만듭니다. Animal 은 자식 클래스인 CatDog 에 의해 확장됩니다.

<?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";
    }
}

이 예제에는 값을 반환하는 메서드가 없다는 점을 주목하세요. 이제 Animal, Cat, Dog 클래스 타입의 새 객체를 반환하는 팩토리(factory) 몇 개를 추가하겠습니다.

<?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();

위 예제의 출력은 다음과 같습니다.

Ricky meows
Mavrick barks

Note: PHP 8.5.0 부터 부모 메서드가 반환 타입으로 static 을 선언한 경우, final 클래스의 오버라이딩 메서드는 대신 self 또는 클래스 자신의 타입을 반환 타입으로 선언할 수 있습니다. 더 이상 확장될 수 없는 클래스에서는 이것들이 static 과 동일하기 때문입니다. 자세한 내용은 마이그레이션 가이드를 참고하세요.

반공변성 (Contravariance)

앞선 예제의 Animal, Cat, Dog 클래스에 이어서 FoodAnimalFood 클래스를 추가하고, Animal 추상 클래스에 eat(AnimalFood $food) 메서드를 추가하겠습니다.

<?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);
    }
}

반공변성의 동작을 확인하기 위해, Dog 클래스에서 eat 메서드를 오버라이드해 어떤 Food 타입 객체든 허용하도록 만듭니다. Cat 클래스는 그대로 둡니다.

<?php

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

다음 예제는 반공변성의 동작을 보여 줍니다.

<?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);

위 예제의 출력은 다음과 같습니다.

Ricky eats AnimalFood
Mavrick eats Food

그렇다면 $kitty$banana 를 eat() 하려 하면 어떻게 될까요?

$kitty->eat($banana);

위 예제의 출력은 다음과 같습니다.

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

프로퍼티의 공변성 (Property variance)

기본적으로 프로퍼티는 공변적이지도 반공변적이지도 않으며, 따라서 불변(invariant) 입니다. 즉 자식 클래스에서 그 타입이 전혀 바뀌지 않을 수 있습니다. 그 이유는 "get" 연산은 공변적이어야 하고, "set" 연산은 반공변적이어야 하기 때문입니다. 프로퍼티가 두 요구 조건을 모두 만족하는 유일한 방법은 불변인 것입니다.

PHP 8.4.0 부터 추상 프로퍼티(인터페이스나 추상 클래스에서)와 가상 프로퍼티(virtual properties)가 추가되면서, get 또는 set 연산만 갖는 프로퍼티를 선언할 수 있게 되었습니다. 그 결과 get 연산만 요구되는 추상 프로퍼티나 가상 프로퍼티는 공변적일 수 있습니다. 마찬가지로 set 연산만 요구되는 추상 프로퍼티나 가상 프로퍼티는 반공변적일 수 있습니다.

그러나 프로퍼티가 get 과 set 연산을 모두 갖게 되면, 추가 확장에 대해 더 이상 공변적이거나 반공변적이지 않게 됩니다. 즉 이제 불변(invariant)이 됩니다.

예제 #1 프로퍼티 타입의 공변성

<?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;
}
?>

더 알아보기

  • 타입 선언(유니언 타입, 인터섹션 타입) 관련 문서
  • 추상 프로퍼티와 가상 프로퍼티 (PHP 8.4)
  • PHP 8.5 마이그레이션 가이드 — static 반환 타입 관련 변경 사항