변성

변성 (Variance)

제네릭 타입을 쓸 때 타입 매개변수의 변성(variance) 이 어떻게 서브타입 관계를 제어하는지 살펴볼게요. +-라는 작은 표기가 어떤 의미를 갖고, 왜 그렇게 설계됐는지가 이 장의 핵심이에요.

출처: Scala 3 Book

본문

타입 매개변수의 변성(variance) 은 매개변수화된 타입(parameterized type, 예: 클래스나 트레이트)의 서브타입 관계를 제어해요.

변성을 설명하기 위해, 다음 타입 정의를 가정할게요.

trait Item { def productNumber: String }
trait Buyable extends Item { def price: Int }
trait Book extends Buyable { def isbn: String }

또한 이런 매개변수화된 타입들도 가정해 볼게요.

// an example of an invariant type
trait Pipeline[T] {
  def process(t: T): T
}

// an example of a covariant type
trait Producer[+T] {
  def make: T
}

// an example of a contravariant type
trait Consumer[-T] {
  def take(t: T): Unit
}
// an example of an invariant type
trait Pipeline[T]:
  def process(t: T): T

// an example of a covariant type
trait Producer[+T]:
  def make: T

// an example of a contravariant type
trait Consumer[-T]:
  def take(t: T): Unit

일반적으로 변성에는 세 가지 모드가 있어요.

  • 불변(invariant) — 기본값이며, Pipeline[T]처럼 써요
  • 공변(covariant)+로 표시하며, Producer[+T]처럼 써요
  • 반변(contravariant)-로 표시하며, Consumer[-T]처럼 써요

이제 이 표기가 무슨 뜻이고 왜 쓰는지 자세히 살펴볼게요.

불변 타입 (Invariant Types)

기본적으로 Pipeline 같은 타입은 타입 인자(여기서는 T)에 대해 불변(invariant)이에요. 즉 Pipeline[Item], Pipeline[Buyable], Pipeline[Book] 같은 타입들은 서로 아무런 서브타입 관계 가 없어요.

그게 당연해요! Pipeline[Buyable] 타입의 값 두 개를 소비하고 가격을 기준으로 그중 하나에 인자 b를 넘기는 다음 메서드를 가정해 볼게요.

def oneOf(
  p1: Pipeline[Buyable],
  p2: Pipeline[Buyable],
  b: Buyable
): Buyable = {
  val b1 = p1.process(b)
  val b2 = p2.process(b)
  if (b1.price < b2.price) b1 else b2
 } 
def oneOf(
  p1: Pipeline[Buyable],
  p2: Pipeline[Buyable],
  b: Buyable
): Buyable =
  val b1 = p1.process(b)
  val b2 = p2.process(b)
  if b1.price < b2.price then b1 else b2

이제 우리 타입들 사이에 이런 서브타입 관계 가 있다는 걸 떠올려 보세요.

Book <: Buyable <: Item

oneOf 메서드에 Pipeline[Book]을 넘길 수는 없어요. 그 구현이 p1p2Buyable 타입의 값으로 호출하거든요. Pipeline[Book]Book을 기대하는데, 이러면 실행 중 오류가 날 수 있어요.

Pipeline[Item]도 넘길 수 없어요. 그 위에서 process를 호출하면 Item을 반환할 뿐이라서요. 그런데 우리는 Buyable을 반환해야 하거든요.

왜 불변일까? (Why Invariant?)

실은 Pipeline 타입은 타입 매개변수 T인자로도 반환 타입으로도 모두 쓰기 때문에 불변이어야 해요. 같은 이유로 Scala 컬렉션 라이브러리의 일부 타입, 예를 들어 ArraySet불변 이에요.

공변 타입 (Covariant Types)

불변인 Pipeline과 달리 Producer 타입은 타입 매개변수 앞에 +를 붙여 공변(covariant) 으로 표시해요. 타입 매개변수를 반환 위치(return position) 에만 쓰기 때문에 이건 타당해요.

공변으로 표시하면 Producer[Buyable]이 기대되는 곳에 Producer[Book]을 넘기거나 반환할 수 있어요. 그리고 실은 이게 건전(sound)해요. Producer[Buyable].make의 타입은 Buyable반환 하겠다고만 약속하죠. make의 호출자로서 우리는 Buyable의 서브타입인 Book도 기꺼이 받아들여요. 즉 적어도 Buyable이기만 하면 되니까요.

이는 다음 예시로 설명할 수 있어요. makeTwo 함수는 Producer[Buyable]을 기대해요.

def makeTwo(p: Producer[Buyable]): Int =
  p.make.price + p.make.price

책용 프로듀서(producer)를 넘겨도 아주 괜찮아요.

val bookProducer: Producer[Book] = ???
makeTwo(bookProducer)

makeTwo 안의 price 호출은 책(book)에 대해서도 여전히 유효해요.

불변 컨테이너를 위한 공변 타입 (Covariant Types for Immutable Containers)

불변 컨테이너를 다룰 때 공변 타입을 자주 마주치게 돼요. 표준 라이브러리의 List, Seq, Vector 같은 것들이 그 예죠.

예를 들어 ListVector는 대략 이렇게 정의돼요.

class List[+A] ...
class Vector[+A] ...

이렇게 하면 List[Buyable]이 기대되는 곳에 List[Book]을 사용할 수 있어요. 직관적으로도 말이 되죠. 살 수 있는 것들의 컬렉션을 기대한다면, 책들의 컬렉션을 줘도 괜찮아야 해요. 책에는 이 예시에서 ISBN 메서드가 하나 더 있지만, 그 추가 능력은 무시해도 되니까요.

반변 타입 (Contravariant Types)

공변으로 표시된 Producer와 달리 Consumer 타입은 타입 매개변수 앞에 -를 붙여 반변(contravariant) 으로 표시해요. 타입 매개변수를 인자 위치(argument position) 에만 쓰기 때문에 이건 타당해요.

반변으로 표시하면 Consumer[Buyable]이 기대되는 곳에 Consumer[Item]을 넘기거나 반환할 수 있어요. 즉 Consumer[Item] <: Consumer[Buyable]이라는 서브타입 관계가 성립해요. Producer 타입에서는 그 반대라서 Producer[Buyable] <: Producer[Item]이었다는 걸 기억하세요.

그리고 실은 이게 건전해요. Consumer[Item].takeItem을 받아들여요. take의 호출자로서 우리는 Buyable도 제공할 수 있어요. BuyableItem의 서브타입이니, 즉 적어도 Item이기만 하면 Consumer[Item]이 기꺼이 받아주거든요.

소비자를 위한 반변 타입 (Contravariant Types for Consumers)

반변 타입은 공변 타입보다 훨씬 드물어요. 우리 예시에서처럼 "소비자(consumer)"라고 생각하면 돼요. 반변으로 표시된 가장 중요한 타입은 아마 함수(function)일 거예요.

trait Function[-A, +B] {
  def apply(a: A): B
}
trait Function[-A, +B]:
  def apply(a: A): B

그 인자 타입 A는 반변 A로 표시돼요—A 타입의 값을 소비하거든요. 반대로 결과 타입 B는 공변으로 표시돼요—B 타입의 값을 생성하니까요.

함수의 변성 표기에 의해 유도되는 서브타입 관계를 보여주는 예시 몇 개를 볼게요.

val f: Function[Buyable, Buyable] = b => b

// OK to return a Buyable where a Item is expected
val g: Function[Buyable, Item] = f

// OK to provide a Book where a Buyable is expected
val h: Function[Book, Buyable] = f

요약 (Summary)

이 절에서 세 가지 서로 다른 변성을 만났어요.

  • 생산자(Producer) 는 전형적으로 공변이며, 타입 매개변수를 +로 표시해요. 불변 컬렉션도 마찬가지예요.
  • 소비자(Consumer) 는 전형적으로 반변이며, 타입 매개변수를 -로 표시해요.
  • 생산자이면서 동시에 소비자인 타입은 불변이어야 하며, 타입 매개변수에 아무 표시도 필요 없어요. Array 같은 가변 컬렉션이 이 범주에 들어가요.

더 알아보기