Tour of Scala

하위 타입 경계

Language

상위 타입 경계가 특정 타입의 서브타입으로 타입을 제한한다면, 하위 타입 경계는 대상 타입을 다른 타입의 슈퍼타입으로 선언한다. T>:A는 타입 파라미터 T나 추상 타입 T가 타입 A의 슈퍼타입임을 나타낸다.

상위 타입 경계를 유용하게 활용할 수 있는 예제를 살펴보자.

case class ListNode[T](h: T, t: ListNode[T]) {
  def head: T = h
  def tail: ListNode[T] = t
  def prepend(elem: T): ListNode[T] =
    ListNode(elem, this)
}

이 프로그램은 앞쪽에 항목을 추가하는 동작을 제공하는 링크드 리스트를 구현하고 있다. 안타깝게도 클래스 ListNode의 타입 파라미터로 사용된 타입은 불변자이고, 타입 ListNode[String]타입 List[Object]의 서브타입이 아니다. 가변성 어노테이션의 도움을 받아서 이런 서브타입 관계의 시맨틱을 표현할 수 있다.

case class ListNode[+T](h: T, t: ListNode[T]) { ... }

하지만 순가변성 어노테이션은 반드시 순가변 위치의 타입 변수로 사용돼야만 하기 때문에, 이 프로그램은 컴파일되지 않는다. 타입 변수 T가 메소드 prepend의 파라미터 타입으로 쓰였기 때문에 이 규칙이 깨지게 된다. 그렇지만 하위 타입 경계의 도움을 받는다면 T가 순가변 위치에만 나타나도록 prepend 메소드를 구현할 수 있다.

다음이 이를 적용한 코드다.

case class ListNode[+T](h: T, t: ListNode[T]) {
  def head: T = h
  def tail: ListNode[T] = t
  def prepend[U >: T](elem: U): ListNode[U] =
    ListNode(elem, this)
}

주의: 새로운 prepend 메소드에선 타입의 제약이 조금 줄어들게 된다. 예를 들어 이미 만들어진 리스트에 슈퍼타입의 객체를 집어 넣을 수도 있다. 그 결과로 만들어지는 리스트는 이 슈퍼타입의 리스트다.

이에 관한 코드를 살펴보자.

object LowerBoundTest extends App {
  val empty: ListNode[Null] = ListNode(null, null)
  val strList: ListNode[String] = empty.prepend("hello")
                                       .prepend("world")
  val anyList: ListNode[Any] = strList.prepend(12345)
}

윤창석, 이한욱 옮김