불투명 타입 별칭: 더 많은 세부 사항

불투명 타입 별칭: 더 많은 세부 사항 (Opaque Type Aliases: More Details)

불투명 타입 별칭의 기본은 앞선 글에서 다뤘고, 여기서는 문법, 타입 체크 규칙, 동등성의 번역, 최상위 불투명 타입처럼 더 깊은 내용을 파고들어요. 투명해지는 스코프가 정확히 어디까지인지가 핵심 포인트예요.

출처: Scala 3 Reference

본문

문법 (Syntax)

Modifier          ::=  ...
                    |  ‘opaque’

opaque는 soft 수식어예요. 정의 키워드 앞에 있지 않으면 일반 식별자로 계속 쓸 수 있어요.

불투명 타입 별칭은 클래스, trait, 또는 객체의 멤버이거나 최상위에 정의되어야 해요. 지역 블록에서는 정의할 수 없습니다.

타입 체크 (Type Checking)

(단형성, monomorphic) 불투명 타입 별칭의 일반적인 형태는 이래요:

opaque type T >: L <: U = R

여기서 하한 L과 상한 U는 빠질 수 있는데, 그 경우 각각 scala.Nothingscala.Any로 간주돼요. 경계가 주어지면 오른쪽 R이 그 경계에 적합한지, 즉 L <: R 그리고 R <: U인지가 검사됩니다. F-경계(F-bounds)는 불투명 타입 별칭에서 지원되지 않아요. TL이나 U에 나타날 수 없습니다.

별칭 정의의 스코프 안에서는 별칭이 투명해요. TR의 보통 별칭으로 취급됩니다. 스코프 밖에서는 별칭이 다음 추상 타입으로 취급돼요:

type T >: L <: U

불투명 타입 별칭이 객체에 정의된 경우 특별한 경우가 생겨요. 예시:

object o:
  opaque type T = R

이 경우 객체 안에서는 (불투명이 아닌 타입에 대해서도) o.TT 또는 그 확장 형태인 o.this.T와 같다는 것을 알 수 있어요. 여기서의 같음은 상호 부분타입, 즉 o.T <: o.this.To.this.T <: o.T를 의미해요. 게다가 불투명 타입 별칭의 규칙에 따라 o.this.TR과 같아요. 두 같음이 합쳐지면서, o 안에서는 o.TR과 같다는 것도 알게 됩니다. 그래서 다음 코드는 타입 체크를 통과해요:

object o:
  opaque type T = Int
  val x: Int = id(2)
def id(x: o.T): o.T = x

불투명 타입 별칭은 private일 수 없고 서브클래스에서 재정의될 수도 없어요. 불투명 타입 별칭은 오른쪽으로 컨텍스트 함수 타입을 가질 수 없습니다.

불투명 타입의 타입 파라미터 (Type Parameters of Opaque Types)

불투명 타입 별칭은 하나의 타입 파라미터 리스트를 가질 수 있어요. 다음 별칭들은 잘 형성된(well-formed) 예시입니다:

opaque type F[T] = (T, T)
opaque type G = [T] =>> List[T]

하지만 다음은 그렇지 않아요:

opaque type BadF[T] = [U] =>> (T, U)
opaque type BadG = [T] =>> [U] =>> (T, U)

동등성의 번역 (Translation of Equality)

불투명 타입의 두 값을 ==!=로 비교하면, 그 타입에 대해 다른 오버로드된 ==!= 연산자가 정의되지 않는 한 보통 보편 동등성(universal equality)을 사용해요. 박싱을 피하기 위해, 이 연산은 타입 체크 후 하부 타입에 정의된 (부)동등 연산자로 매핑됩니다. 예를 들어:

  opaque type T = Int

  ...
  val x: T
  val y: T
  x == y    // uses Int equality for the comparison.

최상위 불투명 타입 (Top-level Opaque Types)

최상위 불투명 타입 별칭은 그것이 나타나는 소스 파일의 다른 모든 최상위 정의에서는 투명하지만, 중첩된 객체와 클래스에서는, 그리고 다른 모든 소스 파일에서는 불투명해요. 예시:

// in test1.scala
opaque type A = String
val x: A = "abc"

object obj:
  val y: A = "abc"  // error: found: "abc", required: A

// in test2.scala
def z: String = x   // error: found: A, required: String

최상위 정의가 자기만의 합성 객체에 놓인다는 점을 떠올리면 이 동작이 명확해져요. 예를 들어 test1.scala의 코드는 이렇게 확장됩니다:

object test1$package:
  opaque type A = String
  val x: A = "abc"

object obj:
  val y: A = "abc"  // error: cannot assign "abc" to opaque type alias A

불투명 타입 별칭 Ax의 정의를 포함하는 스코프에서 투명하지만, objy의 정의에서는 투명하지 않아요.

투명 인라인 메서드 안의 불투명 타입 (Opaque Types in Transparent Inline Methods)

불투명 타입이 정의된 컨텍스트 안에 위치한 투명 인라인 메서드에서 그 불투명 타입을 반환한다면, 추가적인 주의가 필요해요. 메서드 본문의 타입 체크와 타입 추론이 그 컨텍스트의 관점에서 이루어지기 때문에, 반환 타입에는 dealias된(별칭이 풀린) 불투명 타입이 들어갈 수 있어요. 일반적으로 이것은 그런 투명 메서드에 대한 호출이 DECLARED & ACTUAL을 반환한다는 뜻이에요. 여기서 DECLARED는 메서드 선언에 정의된 반환 타입이고, ACTUAL은 인라인 후 반환되는 타입으로 dealias된 불투명 타입을 포함할 수 있죠.

API 설계자는 메서드 본문 안에서 : ExpectedType로 명시적으로 어노테이션하거나, 반환되는 메서드에 타입 파라미터를 명시적으로 전달함으로써 올바른 타입이 반환되게 할 수 있어요. 이렇게 명시적으로 어노테이션하는 것은 가장 바깥쪽 투명 인라인 메서드 호출에는 도움이 되지만, 중첩 호출에는 영향을 주지 않아요. 인라인되는 새 컨텍스트의 관점에서는 컴파일 오류를 피하기 위해 여전히 dealias해야 할 수 있기 때문이죠:

object Time:
  opaque type Time = String
  opaque type Seconds <: Time = String

  // opaque type aliases have to be dealiased in nested calls,
  // otherwise the resulting program might not be typed correctly
  // in the below methods this will be typed as Seconds & String despite
  // the explicit type declaration
  transparent inline def sec(n: Double): Seconds =
    s"${n}s": Seconds

  transparent inline def testInference(): List[Time] =
    List(sec(5)) // infers List[String] and returns List[Time] & List[String], not List[Seconds]
  transparent inline def testGuarded(): List[Time] =
    List(sec(5)): List[Seconds] // returns List[Seconds]
  transparent inline def testExplicitTime(): List[Time] =
    List[Seconds](sec(5)) // returns List[Seconds]
  transparent inline def testExplicitString(): List[Time] =
    List[String](sec(5)) // returns List[Time] & List[String]

end Time

@main def main() =
  val t1: List[String] = Time.testInference() // returns List[Time.Time] & List[String]
  val t2: List[Time.Seconds] = Time.testGuarded() // returns List[Time.Seconds]
  val t3: List[Time.Seconds] = Time.testExplicitTime() // returns List[Time.Seconds]
  val t4: List[String] = Time.testExplicitString() // returns List[Time.Time] & List[String]

특히 인라인되는 것의 타입이 이런 중첩 투명 호출의 타입에 의존한다면 주의하세요.

## Relationship to SIP 35

Opaque types in Scala 3 are an evolution from what is described in
[Scala SIP 35](https://docs.scala-lang.org/sips/opaque-types.html).

The differences compared to the state described in this SIP are:

 1. Opaque type aliases cannot be defined anymore in local statement sequences.
 2. The scope where an opaque type alias is visible is now the whole scope where
    it is defined, instead of just a companion object.
 3. The notion of a companion object for opaque type aliases has been dropped.
 4. Opaque type aliases can have bounds.
 5. The notion of type equality involving opaque type aliases has been clarified. It was
    strengthened with respect to the previous implementation of SIP 35.

더 알아보기 (Learn more)