Enumeration

Enumeration (열거형)

enumeration은 이름 붙은 값들의 집합으로 이루어진 타입을 정의할 때 사용해요.

출처: Scala 3 Reference

본문

enum Color:
  case Red, Green, Blue

이 코드는 새 sealed 클래스 Color를 정의하는데, 여기에는 Color.Red, Color.Green, Color.Blue라는 세 값이 있어요. 색상 값들은 Color의 컴패니언 객체의 멤버예요.

파라미터화된 enum

enum은 파라미터화할 수 있어요.

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)

예시에서 볼 수 있듯이, 명시적인 extends 절을 사용해서 파라미터 값을 정의할 수 있어요.

enum을 위해 정의되는 메서드

enum의 값들은 고유한 정수와 대응돼요. enum 값과 연관된 정수는 ordinal 메서드가 돌려줘요.

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)
val red = Color.Red
val ord = red.ordinal
assert(ord == 0)

enum의 컴패니언 객체는 유틸리티 메서드 세 개도 정의해요. valueOf 메서드는 이름으로 enum 값을 얻고, values 메서드는 enumeration에 정의된 모든 enum 값을 Array로 돌려줘요. fromOrdinal 메서드는 ordinal(Int) 값으로부터 enum 값을 얻어요.

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)
val blue = Color.valueOf("Blue")
// blue: Color = Blue
val values = Color.values
// values: Array[Color] = Array(Red, Green, Blue)
val red = Color.fromOrdinal(0)
// red: Color = Red

enum의 사용자 정의 멤버

enum에 자신만의 정의를 추가할 수도 있어요. 예를 들어볼게요.

enum Planet(mass: Double, radius: Double):
  private final val G = 6.67300E-11
  def surfaceGravity = G * mass / (radius * radius)
  def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity

  case Mercury extends Planet(3.303e+23, 2.4397e6)
  case Venus   extends Planet(4.869e+24, 6.0518e6)
  case Earth   extends Planet(5.976e+24, 6.37814e6)
  case Mars    extends Planet(6.421e+23, 3.3972e6)
  case Jupiter extends Planet(1.9e+27,   7.1492e7)
  case Saturn  extends Planet(5.688e+26, 6.0268e7)
  case Uranus  extends Planet(8.686e+25, 2.5559e7)
  case Neptune extends Planet(1.024e+26, 2.4746e7)
end Planet

enum의 사용자 정의 컴패니언 객체

enum을 위해 명시적인 컴패니언 객체를 정의하는 것도 가능해요.

enum Planet(mass: Double, radius: Double):
  private final val G = 6.67300E-11
  def surfaceGravity = G * mass / (radius * radius)
  def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity

  case Mercury extends Planet(3.303e+23, 2.4397e6)
  case Venus   extends Planet(4.869e+24, 6.0518e6)
  case Earth   extends Planet(5.976e+24, 6.37814e6)
  case Mars    extends Planet(6.421e+23, 3.3972e6)
  case Jupiter extends Planet(1.9e+27,   7.1492e7)
  case Saturn  extends Planet(5.688e+26, 6.0268e7)
  case Uranus  extends Planet(8.686e+25, 2.5559e7)
  case Neptune extends Planet(1.024e+26, 2.4746e7)
end Planet
object Planet:
  def main(args: Array[String]) =
    val earthWeight = args(0).toDouble
    val mass = earthWeight / Earth.surfaceGravity
    for p <- values do
      println(s"Your weight on $p is ${p.surfaceWeight(mass)}")
end Planet

Enum case에 대한 제약

Enum case 선언은 이차 생성자(secondary constructor)와 비슷해요. enum 템플릿 안에 선언되지만 스코프는 템플릿 밖으로 잡히죠. 즉 enum case 선언은 enum 클래스의 내부 멤버에 접근할 수 없어요.

마찬가지로 enum case 선언은 enum의 컴패니언 객체의 멤버를 직접 참조할 수도 없어요. (직접 import했거나 이름을 바꿔 import했더라도 마찬가지예요.) 예를 들어:

import Planet.*
enum Planet(mass: Double, radius: Double):
  private final val (mercuryMass, mercuryRadius) = (3.303e+23, 2.4397e6)

  case Mercury extends Planet(mercuryMass, mercuryRadius)             // Not found
  case Venus   extends Planet(venusMass, venusRadius)                 // illegal reference
  case Earth   extends Planet(Planet.earthMass, Planet.earthRadius)   // ok
object Planet:
  private final val (venusMass, venusRadius) = (4.869e+24, 6.0518e6)
  private final val (earthMass, earthRadius) = (5.976e+24, 6.37814e6)
end Planet

Mercury가 참조하는 필드는 보이지 않고, Venus가 참조하는 필드도 (import Planet.*를 썼더라도) 직접 참조할 수 없어요. Earth에서 보여준 것처럼 간접적인 참조를 써야 해요.

참고로 enum case를 위한 컴패니언 객체는 enum 클래스 템플릿 안에서도, enum 클래스 컴패니언 객체 안에서도 작성할 수 없어요.

enum Planet(mass: Double, radius: Double):
  case Earth extends Planet(5.976e+24, 6.37814e6)
  object Earth:
    // a near-Earth object, but not a companion and not an enum value
    assert(this ne Planet.Earth)
    def report = assert(Planet.this.Earth eq Planet.Earth, s"I thought $this was Earth!")
end Planet

Enum case는 접근 수식자(access modifier)만 받아요. Enum 클래스는 접근 수식자와 함께 intoinfix도 받아요.

Enum case의 폐기(deprecation)

라이브러리 작성자라면 enum case가 더 이상 쓰일 의도가 아님을 알리고 싶을 수 있어요. 하지만 폐기된 case를 특별히 처리하는 것처럼, 공개 API에서 case를 제거해도 우아하게 처리하고 싶을 수도 있죠.

예를 들어 설명할게요. Planet enum에 원래 case가 하나 더 있었다고 해볼게요.

 enum Planet(mass: Double, radius: Double):
   ...
   case Neptune extends Planet(1.024e+26, 2.4746e7)
+  case Pluto   extends Planet(1.309e+22, 1.1883e3)
 end Planet

이제 Pluto case를 폐기하고 싶다고 해볼게요. 먼저 Plutoscala.deprecated 어노테이션을 추가해요.

 enum Planet(mass: Double, radius: Double):
   ...
   case Neptune extends Planet(1.024e+26, 2.4746e7)
-  case Pluto   extends Planet(1.309e+22, 1.1883e3)
+
+  @deprecated("refer to IAU definition of planet")
+  case Pluto extends Planet(1.309e+22, 1.1883e3)
 end Planet

enum Planet이나 object Planet의 어휘적 스코프 밖에서 Planet.Pluto를 참조하면 폐기 경고가 나와요. 하지만 그 스코프 안에서는 여전히 참조할 수 있어서, 폐기된 case에 대한 인트로스펙션을 구현할 수 있어요.

trait Deprecations[T <: reflect.Enum]:
  extension (t: T) def isDeprecatedCase: Boolean

object Planet:
  given Deprecations[Planet]:
    extension (p: Planet)
      def isDeprecatedCase = p == Pluto

라이브러리가 타입 클래스 유도(type class derivation)를 이용해서 Deprecations의 인스턴스를 자동으로 제공하는 상황을 상상해볼 수 있어요.

Java Enum과의 호환성

Scala로 정의한 enum을 Java enum처럼 쓰고 싶다면, 기본으로 import되는 java.lang.Enum 클래스를 다음과 같이 상속하면 돼요.

enum Color extends Enum[Color]:
  case Red, Green, Blue

타입 파라미터는 Java enum 정의에서 오는 것으로, enum의 타입과 같아야 해요. java.lang.Enum을 상속할 때 생성자 인자(Java API 문서에 정의된 것)를 제공할 필요는 없어요——컴파일러가 자동으로 생성해 주거든요.

Color를 그렇게 정의하면, Java enum처럼 사용할 수 있어요.

enum Color extends Enum[Color]:
  case Red, Green, Blue
val cmp = Color.Red.compareTo(Color.Green)
assert(cmp == -1)

Java에서 Scala 3 enum을 사용하는 더 깊은 예시는 이 테스트에서 볼 수 있어요. 그 테스트에서는 enum이 MainScala.scala 파일에 정의되어 있고 Java 소스 Test.java에서 사용돼요.

구현

enum은 scala.reflect.Enum 트레이트를 상속하는 sealed 클래스로 표현돼요. 이 트레이트는 공개 메서드 하나 ordinal을 정의해요.

package scala.reflect

/** A base trait of all Scala enum definitions */
transparent trait Enum extends Any, Product, Serializable:

  /** A number uniquely identifying a case of an enum */
  def ordinal: Int

extends 절을 가진 enum 값은 익명 클래스 인스턴스로 확장돼요. 예를 들어 위의 Venus 값은 다음과 같이 정의될 거예요.

val Venus: Planet = new Planet(4.869E24, 6051800.0):
  def ordinal: Int = 1
  override def productPrefix: String = "Venus"
  override def toString: String = "Venus"

extends 절이 없는 enum 값들은 모두 단일 구현을 공유하는데, 이는 태그와 이름을 인자로 받는 private 메서드로 인스턴스화할 수 있어요. 예를 들어 위의 Color.Red 값의 첫 정의는 다음과 같이 확장될 거예요.

val Red: Color = $new(0, "Red")

참고 자료

더 자세한 내용은 Issue #1970과 PR #4003을 참고하세요.