투명 트레이트와 클래스

투명 트레이트와 클래스 (Transparent Traits and Classes)

트레이트에는 크게 두 가지 쓰임이 있어요. 컴파일러가 추론한 타입에 나타나지 않게 하고 싶은 트레이트는 transparent로 표시할 수 있습니다.

출처: Scala 3 Reference

본문

트레이트는 두 가지 역할로 쓰여요.

  • 다른 클래스와 트레이트를 위한 믹스인(mixin)
  • val, def, 파라미터의 타입 어떤 트레이트는 주로 첫 번째 역할로 쓰이는데, 그런 트레이트는 추론된 타입에 나타나지 않는 게 보통 더 좋아요. 대표적인 예가 컴파일러가 모든 case class·case object에 믹스인으로 추가하는 Product 트레이트예요. Scala 2에서는 이 부모 트레이트 때문에 추론된 타입이 필요 이상으로 복잡해지기도 했어요. 예시를 볼게요.
trait Kind
case object Var extends Kind
case object Val extends Kind
val x = Set(if condition then Val else Var)

여기서 x의 추론된 타입은 Set[Kind & Product & Serializable]이 되는데, 우리가 기대하는 건 Set[Kind]였잖아요. 이런 타입이 추론되는 이유는 다음과 같아요.

  • 위 조건식(conditional)의 타입은 유니언 타입 Val | Var이에요. 이 유니언 타입은 "소프트(soft)"로 취급돼요. 즉 소스 프로그램에 명시적으로 적힌 게 아니라, 어떤 대안들의 타입들로 상한(upper bound)을 만들면서 생겨난 거죠.
  • 소프트 유니언 타입은 타입 추론에서 그 유니언 타입의 슈퍼타입인 클래스·트레이트 타입들의 최소 곱(least product)으로 넓혀져요. 이 예에서는 세 트레이트 모두 ValVar의 슈퍼 트레이트이므로 그 타입이 Kind & Product & Serializable이 돼요. 그래서 이 타입이 셋(set)의 원소 타입으로 추론되는 거예요. Scala 3에서는 트레이트나 클래스를 transparent로 표시할 수 있는데, 그러면 타입 추론에서 그걸 감출(suppress) 수 있어요. 위 코드와 같은 구성을 따라가되, Product 대신 새 투명 트레이트 S를 쓰는 예시를 볼게요.
transparent trait S
trait Kind
object Var extends Kind, S
object Val extends Kind, S
val x = Set(if condition then Val else Var)

이제 xSet[Kind] 타입으로 추론돼요. 공통의 투명 트레이트 S는 추론된 타입에 나타나지 않죠. 앞의 예시에서 Kind를 투명하게 선언할 수도 있어요.

transparent trait Kind

그러면 if condition then Val else Var의 넓혀진 유니언 타입에는 투명 트레이트 KindS만 남아요. 이 경우 넓히기가 아예 수행되지 않아서, xSet[Val | Var] 타입을 가져요. 루트 클래스와 트레이트인 Any, AnyVal, Object, Matchable은 투명한 것으로 간주돼요. 그래서 이런 표현은

if condition then 1 else "hello"

넓혀진 타입 Any 대신 Int | String 타입을 갖게 돼요.

어떤 트레이트와 클래스가 투명한가? (Which Traits and Classes Are Transparent?)

트레이트와 클래스는 transparent 수식어를 붙여 투명하게 선언해요. Scala 2의 트레이트·클래스도 @transparentTrait 애너테이션으로 투명하게 선언할 수 있었는데, 이 애너테이션은 scala.annotation에 정의되어 있어요. 3.8.0부터 deprecated되었고, 타입 추론에는 아무 영향이 없어요. 다음 클래스와 트레이트는 자동으로 투명하게 취급돼요.

    scala.Any
    scala.AnyVal
    scala.Matchable
    scala.Product
    java.lang.Object
    java.lang.Comparable
    java.io.Serializable

보통 루트 클래스가 아닌 투명 타입은, 상속받는 클래스·트레이트의 구현에 영향을 주면서도 그 자체로 타입으로는 좀처럼 쓰이지 않는 트레이트예요. 표준 컬렉션 라이브러리의 두 예를 꼽자면

  • IterableOpsIterable을 위한 메서드 구현을 제공해요.
  • StrictOptimizedSeqOps — 효율적인 인덱싱을 지원하는 시퀀스에 대해 그 구현 중 일부를 최적화해요. 일반적으로, 재귀적으로 확장되는 트레이트라면 투명하게 선언하기 좋은 후보예요.

추론을 위한 규칙 (Rules for Inference)

투명 트레이트와 클래스는 평소처럼 명시적 타입으로 줄 수 있어요. 다만 타입을 추론할 때는 종종 생략돼요. 대략적으로 타입 추론 규칙은 다음을 의미해요.

  • 투명 트레이트는 가능하면 교집합(intersection)에서 빠져요.
  • 넓히면 투명 슈퍼타입만 남게 되는 경우에는 유니언 타입을 넓히지 않아요. 정확한 규칙은 다음과 같아요.
  • 타입 변수의 타입, val의 타입, def의 반환 타입을 추론할 때 —
    • 그 타입이 higher-kinded가 아니고,
    • B가 그 타입의 알려진 상한(없으면 Any)이라고 했을 때:
    • 지금까지 추론된 타입이 T1 & ... & Tn(n >= 1) 형태라면, 결과 타입이 여전히 바운드 B의 서브타입이 되도록 최대한 많은 투명 트레이트 TiAny로 바꿔요.
    • 단, 모든 타입 Ti가 그렇게 바뀔 수 있다면 이 넓히기는 수행하지 않아요. 이 조항 덕분에 Product 같은 단일 투명 트레이트 인스턴스가 Any로 넓혀지지 않아요. 투명 트레이트 인스턴스는 다른 어떤 타입과 함께 나타날 때만 빠져요.
    • 원래 타입이 유니언 타입인데 이전 단계에서 투명 트레이트·클래스만으로 이루어진 곱으로 넓혀졌다면, 넓혀진 형태 대신 원래 유니언 타입을 유지해요.