Enums와 ADTs의 변환

Enums와 ADTs의 변환 (Translation)

컴파일러는 enum과 그 case들을 오직 Scala의 다른 언어 기능만을 사용하는 코드로 확장해요. 그런 의미에서 Scala의 enum은 편리한 문법적 설탕(syntactic sugar)이지만, Scala의 핵심을 이해하는 데 필수적이지는 않아요.

출처: Scala 3 Reference

본문

이제 enum이 어떻게 확장되는지 자세히 살펴볼게요. 먼저 몇 가지 용어와 표기 규칙을 정리할게요.

  • E는 enum의 이름으로, CE에 나타나는 case의 이름으로 쓰겠어요.
  • <...>는 어떤 상황에서는 비어 있을 수도 있는 문법 구조를 나타내요. 예를 들어 <value-params>는 하나 이상의 파라미터 목록 (...)이거나 아무것도 없는 상태를 뜻해요.
  • Enum case는 세 가지 범주로 나뉘어요.
    • 클래스 case(Class cases) — 타입 파라미터 구간 [...] 또는 하나 이상의 (비어 있을 수도 있는) 파라미터 구간 (...)으로 파라미터화된 case를 말해요.
    • 단순 case(Simple cases) — 제네릭이 아닌 enum의 case로, 파라미터도 extends 절도 본문도 없는 경우예요. 즉 이름만으로 이루어져요.
    • 값 case(Value cases) — 파라미터 구간은 없지만 (생성될 수도 있는) extends 절과/또는 본문을 가진 모든 case를 말해요.

단순 case와 값 case를 통틀어 **싱글턴 case(singleton cases)**라고 불러요.

탈-설탕 규칙을 따르면 클래스 case는 case class로, 싱글턴 case는 val 정의로 매핑돼요.

탈-설탕 규칙은 모두 아홉 개예요. 규칙 (1)은 enum 정의를, 규칙 (2)와 (3)은 단순 case를 탈-설탕해요. 규칙 (4)부터 (6)까지는 빠져 있는 case들의 extends 절을 정의하고, 규칙 (7)부터 (9)까지는 그런 extends 절을 가진 case들이 어떻게 case classval로 매핑되는지 정의해요.

  1. enum 정의
    enum E ... { <defs> <cases> }
    
    scala.reflect.Enum 트레이트를 상속하는 sealed abstract 클래스와, 정의된 case들을 담고 있는 연관 컴패니언 객체(아래 규칙 2~8에 따라 확장)로 확장돼요. enum 클래스는 컴파일러가 생성한 import로 시작하는데, 그 import는 모든 case의 이름 <caseIds>를 가져와서 클래스 안에서 접두사 없이 사용할 수 있게 해줘요.
    sealed abstract class E ... extends <parents> with scala.reflect.Enum {
      import E.{ <caseIds> }
      <defs>
    }
    object E { <cases> }
    
  2. 쉼표로 구분된 enum 이름 목록으로 이루어진 단순 case
    case C_1, ..., C_n
    
    는 다음과 같이 확장돼요.
    case C_1; ...; case C_n
    
    원래 case에 붙어 있던 수식자(modifier)나 어노테이션은 확장된 모든 case로 이어져요.
  3. 타입 파라미터를 받지 않는 enum E의 단순 case
    case C
    
    는 다음과 같이 확장돼요.
    val C = $new(n, "C")
    
    여기서 $newE의 인스턴스를 만드는 private 메서드예요(아래 참고).
  4. E가 타입 파라미터를 가진 enum이라면
    V1 T1 >: L1 <: U1 ,   ... ,    Vn Tn >: Ln <: Un      (n > 0)
    
    여기서 각 변성(variance) Vi'+' 또는 '-' 중 하나예요. 그러면 단순 case
    case C
    
    는 다음과 같이 확장돼요.
    case C extends E[B1, ..., Bn]
    
    여기서 BiVi = '+'일 때 Li, Vi = '-'일 때 Ui예요. 이 결과는 다시 규칙 (8)로 다시 쓰여져요. 비-변성 타입 파라미터를 가진 enum의 단순 case는 허용되지 않아요. (단, 명시적인 extends 절을 가진 값 case는 허용돼요.)
  5. 타입 파라미터를 받지 않는 enum E에서, extends 절이 없는 클래스 case
    case C <type-params> <value-params>
    
    는 다음과 같이 확장돼요.
    case C <type-params> <value-params> extends E
    
    이 결과는 다시 규칙 (9)로 다시 쓰여져요.
  6. E가 타입 파라미터 Ts를 가진 enum이라면, 타입 파라미터도 extends 절도 없는 클래스 case
    case C <value-params>
    
    는 다음과 같이 확장돼요.
    case C[Ts] <value-params> extends E[Ts]
    
    이 결과는 다시 규칙 (9)로 다시 쓰여져요. 타입 파라미터를 스스로 가진 클래스 case는 extends 절을 명시적으로 줘야 해요.
  7. E가 타입 파라미터 Ts를 가진 enum이라면, 타입 파라미터는 없고 extends 절은 있는 클래스 case
    case C <value-params> extends <parents>
    
    는 다음과 같이 확장돼요.
    case C[Ts] <value-params> extends <parents>
    
    단, 파라미터 Ts 중 적어도 하나가 <value-params>의 파라미터 타입이나 <parents>의 타입 인자에 언급되어야 해요.
  8. 값 case
    case C extends <parents>
    
    E의 컴패니언 객체 안의 값 정의로 확장돼요.
    val C = new <parents> { <body>; def ordinal = n }
    
    여기서 n은 컴패니언 객체 안에서 그 case의 서수(ordinal) 번호로, 0부터 시작해요. 익명 클래스는 Enum에서 상속한 추상 Product 메서드들도 구현해요. 값 case가 <parents>의 타입 인자에서 감싸고 있는 enum의 타입 파라미터를 참조하면 오류예요.
  9. 클래스 case
    case C <params> extends <parents>
    
    E의 컴패니언 객체 안에서 final case class와 유사하게 확장돼요.
    final case class C <params> extends <parents>
    
    이 enum case는 다음과 같은 형태의 ordinal 메서드를 정의해요.
    def ordinal = n
    
    여기서 n은 컴패니언 객체 안에서 그 case의 서수 번호로, 0부터 시작해요. 값 case가 <params>의 파라미터 타입이나 <parents>의 타입 인자에서 감싸고 있는 enum의 타입 파라미터를 참조하면 오류예요. (단, 그 파라미터가 이미 case 자신의 타입 파라미터라면——즉 파라미터 이름이 <params>에 정의되어 있다면——괜찮아요.) enum case의 컴파일러 생성 applycopy 메서드
    case C(ps) extends P1, ..., Pn
    
    는 특별히 처리돼요. apply 메서드에 대한 호출 C(ts)는 적용 지점의 기대 타입과 여전히 호환되는 한 (투명 트레이트는 제외하고) 기저 타입 P1 & ... & Pn으로 간주돼요. Ccopy 메서드에 대한 호출 t.copy(ts)도 같은 방식으로 처리돼요.

싱글턴 case를 가진 enum의 변환

하나 이상의 싱글턴 case를 정의하는 enum E(제네릭일 수도 있어요)는 컴패니언 객체에 다음과 같은 추가 합성 멤버들을 정의해요. (E'E에서 타입 파라미터를 모두 와일드카드로 바꾼 것을 나타내요.)

  • 메서드 valueOf(name: String): E' — 식별자가 name인 싱글턴 case 값을 돌려줘요.
  • 메서드 valuesE가 정의한 모든 싱글턴 case 값의 Array[E']를 정의 순서대로 돌려줘요.

E가 단순 case를 적어도 하나 포함하면, 컴패니언 객체는 추가로 다음을 정의해요.

  • 주어진 서수 번호와 이름으로 새 단순 case 값을 만드는 private 메서드 $new. 이 메서드는 다음과 같이 정의된다고 생각하면 돼요.
    private def $new(_$ordinal: Int, $name: String) =
      new E with runtime.EnumValue:
        def ordinal = _$ordinal
        override def productPrefix = $name // if not overridden in `E`
        override def toString = $name      // if not overridden in `E`
    
    익명 클래스는 Enum에서 상속한 추상 Product 메서드들도 구현해요. ordinal 메서드는 enum이 java.lang.Enum을 상속하지 않을 때만 생성돼요(Scala enum은 명시적으로 지정하지 않는 한 java.lang.Enum을 상속하지 않아요). 상속한다면 java.lang.Enumordinal을 정의하므로 생성할 필요가 없어요. 마찬가지로 java.lang.Enum에서 name을 기준으로 정의되는 toString도 재정의할 필요가 없어요. 마지막으로 productPrefixEjava.lang.Enum을 상속할 때 this.name을 호출해요.

Enum case의 스코프

enum 안의 case는 이차 생성자(secondary constructor)와 비슷하게 취급돼요. this로 감싸고 있는 enum에 접근할 수도 없고, 단순 식별자로 그 값 파라미터나 인스턴스 멤버에 접근할 수도 없어요.

번역된 enum case는 enum의 컴패니언 객체에 위치하지만, this나 단순 식별자로 이 객체나 그 멤버를 참조하는 것도 불법이에요. 컴파일러는 enum case를 감싸고 있는 컴패니언 객체의 스코프에서 타입검사하지만, 그런 불법 접근은 오류로 표시해요.

Java 호환 enum의 변환

Java 호환 enum은 java.lang.Enum을 상속하는 enum이에요. 변환 규칙은 위와 같되, 이 절에서 정의하는 제약이 적용돼요.

Java 호환 enum이 클래스 case를 가지는 것은 컴파일 타임 오류예요.

case C 같은 case는 val이 아니라 @static val로 확장돼요. 이렇게 하면 enum 타입의 정적 필드로 생성되어, Java enum과 동일한 방식으로 표현되도록 보장해요.

그 외 규칙

  • enum case에서 만들어진 것이 아닌 일반 case classscala.reflect.Enum을 상속할 수 없어요. 이렇게 해서 enum의 case는 enum 안에 명시적으로 선언된 것뿐임을 보장해요.
  • enum case가 extends 절을 가진다면, enum 클래스가 상속하는 클래스 중 하나여야 해요.