Stephan Boyer의 What are covariance and contravariance?을 번역한 글이다.
공변성과 반공변성은 무엇인가?
서브타이핑은 프로그래밍 언어 이론에서 까다로운 주제다. 공변성과 반공변성은 오해하기 쉬운 주제이기 때문에 까다롭다. 이 글에서는 이 용어를 설명하려고 한다.
이 글에서는 다음과 같은 표기법을 사용한다.
A <: B
는A
가B
의 서브타입이라는 뜻이다.A -> B
는 함수 타입으로 함수의 인자 타입은A
며 반환 타입은B
라는 의미다.
동기부여 질문
다음과 같은 세 타입이 있다고 가정하자.
Greyhound <: Dog <: Animal
Greyhound
는 Dog
의 서브타입이고 Dog
는 Animal
의 서브타입이다. 서브타입은 일반적으로 추이적 관계(transitive)를 갖는다. 그래서 Greyhound
도 Animal
의 서브타입이라 할 수 있다.
여기서 질문이다. 다음 중 Dog -> Dog
의 서브타입이 될 수 있는 경우는 어느 것일까?
Greyhound -> Greyhound
Greyhound -> Animal
Animal -> Animal
Animal -> Greyhound
이 질문에 어떻게 답할 수 있을까? Dog -> Dog
함수를 인자로 받는 f
함수를 살펴보자. 반환 타입에 대해서는 크게 생각하지 않는다. 구체적으로 적어보면 다음과 같다. f : (Dog -> Dog) -> String
.
이제 f
를 다른 함수인 g
와 함께 호출해보자. g
에는 위에서 나열했던 각각의 함수를 넣어서 어떤 일이 나타나는지 관찰한다.
g : Greyhound -> Greyhound
로 가정하면 f(g)
는 타입 안전(type safe)한가?
아니다. 함수 f
는 인자 g
를 사용하면서 Dog
의 다른 서브타입, 예를 들면 GermanShepherd
를 사용해서 호출할 수도 있기 때문이다.
g : Greyhound -> Animal
로 가정하면 f(g)
는 타입 안전한가?
아니다. 1과 동일한 이유다.
g : Animal -> Animal
로 가정하면 f(g)
는 타입 안전한가?
아니다. f
에서 인자 g
를 호출하면서 개가 어떻게 짖는지 그 반환 값을 얻으려고 할 수 있다. 하지만 모든 Animal
이 짖는 것은 아니다.
g : Animal -> Greyhound
로 가정하면 f(g)
는 타입 안전한가?
그렇다. 이 경우는 안전하다. f
함수는 인자인 g
를 호출할 때 어떤 종류의 Dog
든 사용할 수 있다. 모든 Dog
는 Animal
이기 때문이다. 또한 반환값은 Dog
로 가정할 수 있는데 모든 Greyhound
가 Dog
이기 때문이다.
무슨 일이 일어나고 있나요?
결과적으로 다음 경우에 안전하다.
(Animal -> Greyhound) <: (Dog -> Dog)
반환 타입은 간단하다. Greyhound
는 Dog
의 서브타입이다. 하지만 인자 타입은 반대다. Animal
은 Dog
의 수퍼타입이다!
이 독특한 동작 방식을 적당한 용어를 사용해서 설명한다. 함수 타입에서 반환 타입은 공변적(covariant) 이고, 인자 타입은 반공변적(contravariant) 이다. 반환 타입의 공변성은 A <: B
가 (T -> A) <: (T -> B)
로 적용된다는 뜻이다. (A
는 <:
의 좌측에, B
는 우측에 남아 있다.) 인자 타입의 반공변성은 A <: B
가 (B -> T) <: (A -> T)
로 적용된다는 의미다. (A
와 B
가 위치를 바꾸게 된다.)
재미있는 사실 타입스크립트에서는 인자 타입이 이변적(bivariant), 즉 공변성과 반공변성을 동시에 지닌다. 뭔가 말이 안되는거 같겠지만 말이다. (TypeScript 2.6부터 --strictFunctionTypes
또는 --strict
를 사용하면 이 문제를 해결할 수 있다.) Eiffel은 인자 타입을 반공변적이 아닌 공변적으로 잘못 구현했다.
다른 타입은?
또 다른 질문이다. List<Dog>
는 List<Animal>
의 서브타입이 될 수 있을까?
답변하기 좀 미묘하다. 만약 목록이 불변이면 맞다고 답할 수 있다. 하지만 가변적이라면 당연히 안전하지 않다!
이유를 생각해보자. 나는 List<Animal>
이 필요한데 List<Dog>
를 넘겨줬다고 해보자. 나는 당연히 List<Animal>
을 갖고 있다고 생각하고 Cat
을 집어넣었다. 이제 List<Dog>
에 Cat
이 들어있게 된다! 타입 시스템은 이런 동작을 허용하지 않을 것이다.
불변 목록의 타입이라면 타입 파라미터가 공변적일 수 있지만 가변 목록의 타입은 반드시 공변적이지도, 반공변적이지도 않아야(invariant) 한다.
재미있는 사실 자바에서의 배열은 가변적이면서도 공변적이다. 물론 부적절하다.
추가로, 원문의 덧글 중에 시각적으로 잘 설명하는 자료가 있었다.