하위 타입과 공변·반변(Subtyping and variance)
A가 B의 하위 타입(A ≤ B)이면 B가 필요한 곳 어디에나 A를 써도 된다. 이 규칙을 목록·배열·함수(function) 같은 타입(type) 생성자로 넓히면, 값을 꺼내 주기(period)만 하는 자리는 방향을 따르고(공변, covariant), 받아들이기만 하는 자리는 방향을 뒤집으며(반변, contravariant), 둘 다 하는 자리는 어느 쪽도 허락하지 않는다(무변, invariant).
고양이는 동물입니다. 그래서 동물을 받는 함수 '이름 부르기'에 고양이를 넘겨도 됩니다. 타입으로 적으면
그렇다면 고양이의 목록은 동물의 목록일까요? 목록을 읽기만 한다면 그렇습니다. 동물을 기대하고 꺼낸 것이 고양이여도 아무 문제가 없습니다. 목록에 무엇을 넣을 수 있다면 사정이 다릅니다. 자바에서 Cat[] cats를 만들고 Animal[] animals = cats;로 같은 배열을 동물의 배열이라고 부른 뒤 animals[0] = new Dog();를 쓰면, 컴파일러(compiler)는 세 줄을 모두 통과시킵니다. 자바는 고양이 ≤ 동물이면 Cat[] ≤ Animal[]이라는 규칙을 두기 때문입니다. 그대로 두면 고양이 배열에 개가 들어가고, 나중에 cats[0]을 고양이로 꺼내 쓰는 순간 무너집니다. 그래서 자바는 셋째 줄을 실행할 때 검사해 ArrayStoreException을 던집니다. 타입 검사를 통과한 프로그램이 실행 중에 타입 오류로 멈추니, 정적 타입 검사만 놓고 보면 이 규칙은 건전하지 않습니다. 자바는 그 구멍을 실행 중 검사로 메우는 것입니다. 타입스크립트의 배열도 같은 규칙을 쓰는데, 실행 중 검사가 없습니다. 그래서 개를 고양이로 꺼내 고양이에게만 있는 기능을 쓰는 곳에 가서야 오류가 나거나, 오류 없이 틀린 결과가 나옵니다. 편의를 위해 설계자들이 알고 받아들인 구멍입니다.
함수 타입의 규칙이 이 모든 것의 열쇠입니다. 어떤 동물이든 진찰하는 수의사
'누가 무엇을 공급하는가'로 기억하면 틀리지 않습니다. 인자는 함수를 쓰는 쪽이 공급하므로, 함수는 약속보다 더 넓은 것을 받아들일 수 있어야 합니다. 결과는 함수가 공급하므로, 약속보다 더 좁은(더 구체적인) 것을 돌려줘도 됩니다. 흔한 실수는 인자도 공변이라고 생각하는 것입니다. 에펠(Eiffel) 언어는 메서드를 물려받아 고칠 때 인자 타입을 더 구체적으로 바꾸는 것을 허락했는데, 1989년 윌리엄 쿡이 이것이 실행 중 타입 오류를 낳는 구멍임을 지적했습니다.
타입 생성자 F(목록, 배열, 함수 등)가 하위 타입 관계를 어떻게 옮기는지를 그 생성자의 변성(variance)이라 합니다.
표를 보면 변성이 연산에서 나온다는 것이 드러납니다. 값을 꺼내 주는 연산(get, 함수의 결과)은 Array<Cat>과 Array<Animal>은 어느 방향으로도 하위 타입이 아닙니다. 연속
언어들은 이것을 표시로 받아들였습니다. C#은 2010년 4.0판부터 제네릭 인터페이스의 타입 매개변수(parameter)에 out(꺼내기만 함, 공변)과 in(받기만 함, 반변)을 붙이게 하고, 표시와 맞지 않게 쓰면 컴파일을 거절합니다. 스칼라의 +T와 -T, 코틀린의 out과 in도 같습니다. 자바의 제네릭은 쓰는 곳에서 List<? extends Animal>(꺼내기용: null 말고는 넣을 수 없음)과 List<? super Cat>(넣기용: 꺼낸 값은 Object로만 보임)으로 적게 합니다. 타입스크립트는 2017년 2.6판부터 strictFunctionTypes 설정에서 함수 타입의 인자를 반변으로 검사하지만, 메서드의 인자는 여전히 두 방향을 모두 허락합니다.
범주론(category theory)으로 보면 이 모든 것이 한 문장으로 줄어듭니다. 하위 타입 관계는 순서이고, 순서는 두 대상 사이에 화살표가 많아야 하나인 범주입니다. 공변 생성자는 이 범주(category)에서 자기 자신으로 가는 함자(functor), 곧 순서를 지키는 사상이고, 반변 생성자는 화살표를 뒤집는 반변 함자입니다.
하위 타입을 이름으로 정하는 언어와 모양으로 정하는 언어가 있습니다. 자바와 C#에서는 선언할 때 '고양이는 동물을 물려받는다'고 적어야 하위 타입이 됩니다(이름 기반). 타입스크립트에서는 필드를 더 많이 가진 레코드가 그대로 하위 타입입니다. {이름, 나이}를 가진 값은 {이름}만 요구하는 자리에 쓸 수 있습니다(구조적, 너비 하위 타입). 필드의 타입을 더 구체적으로 바꾸는 깊이 하위 타입은 필드를 읽기만 할 때만 안전합니다. 이유는 배열과 같습니다. 또 코드를 물려받는 상속과 대신 써도 된다는 하위 타입은 다른 개념입니다. 물려받은 메서드의 인자를 좁히면 상속은 되어도 하위 타입은 깨집니다.
하위 타입은 다형성(polymorphism)과 만나면 어려워집니다. '동물의 하위 타입인 모든 X에 대해'라는 뜻의
흔한 오해 하나. 타입을 지금 담긴 값들의 집합으로만 읽으면, 고양이만 든 배열도 동물이 든 배열이니 배열⟨고양이⟩ ≤ 배열⟨동물⟩처럼 보입니다. 그러나 배열⟨동물⟩이라는 타입은 '동물을 꺼낼 수 있다'뿐 아니라 '아무 동물이나 넣을 수 있다'도 약속합니다. 하위 타입은 담긴 값이 아니라 모든 연산에서 '대신 써도 되는가'라는 행동의 약속이고, 리스코프가 행동을 앞세워 정식화한 까닭이 이것입니다. 리스코프와 윙의 조건을 메서드의 명세로 적으면, 하위 타입의 메서드는 전조건(precondition)을 약하게(더 많은 입력을 받게) 하고 후조건(postcondition)을 강하게(더 좁은 결과를 약속하게) 해도 됩니다. 함수 규칙의 반변·공변과 똑같은 모양이며, 호어 논리(Hoare logic)의 결과 규칙이 바로 이 모양입니다.
이어지는 곳. 공변과 반변은 함자 페이지의 공변 함자와 반변 함자를 순서라는 가장 작은 범주에서 본 것이고, 함수 타입의 두 자리는 요네다 보조정리의 Hom 함자로 이어집니다. 하위 타입 규칙이 건전한지는 타입 이론(type theory)의 안전성 정리(진행과 보존)로 따지며, 자바 배열은 실행 중 검사 없이는 그 정리가 깨지는 곳입니다. 곱과 합이 왜 공변인지는 대수적 자료형에서, 타입 변수에 상한을 두는 일은 다형성과 시스템 F에서, 부등식을 푸는 추론의 어려움은 타입 추론에서 이어집니다. 전조건은 반변, 후조건은 공변이라는 명세의 규칙은 호어 논리에서 증명의 규칙으로 다시 나오고, 명제들 사이의 '약하다·강하다'라는 순서는 불 대수(Boolean algebra)의 함의 순서입니다.
이 개념을 언급하는 페이지
- 타입 이론
… 무엇이든 괴델의 불완전성 정리의 한계를 받으며, 타입 이론도 예외가 아닙니다. 하위 타입과 변성은하위 타입과 변성에서, 자원을 정확히 한 번씩 쓰는 타입은 선형 논리에서, 재귀로 정의한 프로그램이 무엇을 뜻하는지는 …
- 대수적 자료형
… 것은 그 페이지에서, 곱과 합이 두 자리 모두에서 공변이라 읽기 전용 자료가 대개 공변이라는 것은하위 타입과 변성에서 이어집니다.
- 타입 추론: 힌들리–밀너
… 방정식 대신 부등식 X\le Y 를 풀어야 해서 가장 일반적인 타입이 깔끔하게 나오지 않는데, 그 어려움은하위 타입과 변성에서 이어집니다.
- 다형성과 시스템 F
… 양화 \forall X\le T 를 더한 F <: 에서는 하위 타입 판정조차 결정할 수 없게 되고(하위 타입과 변성), 시스템 F를 만든 지라르는 1987년 자원을 한 번씩만 쓰는 선형 논리를 내놓았습니다.
- 함자
… 수 있습니다. 공변 함자와 반변 함자의 구별은 프로그래밍에서 하위 타입의 공변·반변으로 그대로 나타나고(하위 타입과 변성), 열린 집합들의 순서에서 집합의 범주로 가는 반변 함자가 층의 출발점인 준층입니다.
- 호어 논리와 프로그램 검증
… 또는, 아니다'로 계산하는 불 대수의 원소이고, 전조건은 약하게 후조건은 강하게라는 결과 규칙은하위 타입의 반변·공변과 같은 모양입니다. 메모리를 자원처럼 나눠 쓰는 분리 논리는 선형 논리와 닮았고, 명세를 타입에 적어 …