교집합 타입

교집합 타입 (Intersection Types)

타입에 & 연산자를 쓰면 **교집합 타입(intersection type)**이 만들어져요. S & T 타입은 타입 S이면서 동시에 타입 T인 값을 나타내죠.

trait Resettable:
  def reset(): Unit

trait Growable[T]:
  def add(t: T): Unit

def f(x: Resettable & Growable[String]) =
  x.reset()
  x.add("first")

출처: Scala 3 Reference

본문

타입 검사 (Type Checking)

위 예시에서 파라미터 xResettable이면서 동시에 Growable[String]이어야 해요. 그래서 x.reset()x.add("first")를 모두 호출할 수 있는 거죠.

교집합 타입 A & B의 멤버는 A의 모든 멤버와 B의 모든 멤버예요. 예를 들어 Resettable & Growable[String]에는 메서드 resetadd가 멤버로 있죠.

&는 교환 법칙이 성립해요. A & BB & A와 같은 타입이에요.

만약 어떤 멤버가 AB 양쪽에 모두 나타난다면, A & B에서 그 멤버의 타입은 A에서의 타입과 B에서의 타입의 교집합이 돼요. 예를 들어 다음 정의를 생각해 볼게요:

trait A:
  def children: List[A]

trait B:
  def children: List[B]

val x: A & B = new C
val ys: List[A & B] = x.children

A & B에서 children의 타입은 A에서의 children 타입과 B에서의 타입의 교집합, 즉 List[A] & List[B]가 돼요. 여기서 List는 공변(covariant)이라서 List[A & B]로 더 단순화할 수 있죠.

궁금할 수 있는 점이 하나 있어요. 주어진 건 각각 List[A] 타입과 List[B] 타입의 children 정의인데, 컴파일러는 어떻게 List[A & B] 타입의 children 정의를 만들어 낼까요? 답은 간단해요. 컴파일러가 그럴 필요가 없다는 거예요. A & B는 그저 "그 타입의 값이 지켜야 할 요구사항들의 집합"을 나타내는 타입일 뿐이거든요. 값이 실제로 만들어지는 지점에서, 모든 상속된 멤버가 올바르게 정의되도록 보장하면 돼요. 그래서 AB를 상속하는 클래스 C를 정의한다면, 그 지점에서 필요한 타입의 children 메서드를 직접 정의해 줘야 해요:

class C extends A, B:
  def children: List[A & B] = ???