대수적 데이터 타입

대수적 데이터 타입 (Algebraic Data Types)

enum 개념은 대수적 데이터 타입(ADT)과 그 일반화된 버전(GADT)까지도 충분히 담아낼 만큼 일반적이에요. Option 타입을 ADT로 어떻게 표현하는지부터 살펴볼게요.

출처: Scala 3 Reference

본문

enum 개념은 대수적 데이터 타입(ADT)과 일반화된 버전(GADT)까지도 지원할 만큼 일반적이에요. 다음은 Option 타입을 ADT로 나타내는 예시예요:

enum Option[+T]:
  case Some(x: T)
  case None

이 예시는 공변(covariant) 타입 파라미터 T를 가진 Option enum을 소개하는데, SomeNone 두 개의 케이스로 이뤄져요. Some은 값 파라미터 x로 매개변수화돼요. 이는 Option을 확장하는 case class를 쓰는 것의 축약형이에요. None은 매개변수화되지 않으므로 보통의 enum 값으로 취급됩니다.

위 예시에서 생략된 extends 절은 명시적으로 적어줄 수도 있어요:

enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[Nothing]

None 값의 부모 타입이 Option[Nothing]으로 추론된다는 점을 주목하세요. 일반적으로 enum 클래스의 모든 공변 타입 파라미터는 컴파일러가 생성하는 extends 절에서 최소화되고, 반대로 모든 반변(contravariant) 타입 파라미터는 최대화돼요. 만약 Option이 무변동(non-variant)이라면 None의 extends 절을 명시적으로 적어줘야 할 거예요.

보통의 enum 값처럼, enum의 케이스는 모두 enum의 컴패니언 객체 안에 정의돼요. 그래서 정의를 임포트로 "끄집어내지" 않는 한 Option.SomeOption.None으로 쓰게 됩니다:

enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[Nothing]
val option = Option.Some("hello")
// option: Option[String] = Some(hello)

val none = Option.None
// none: Option[Nothing] = None

위 표현식들의 타입이 항상 Option이라는 점을 주목하세요. 일반적으로 enum 케이스 생성자 적용의 타입은, 더 구체적인 타입이 기대되지 않는 한, 근본 enum 타입으로 넓어져요. 이것은 보통의 case class와의 미묘한 차이예요. 케이스를 이루는 클래스 자체는 실제로 존재하며, new로 직접 생성하거나 기대 타입을 명시적으로 주는 방식으로 드러낼 수 있어요.

enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[Nothing]
val some0 = new Option.Some(2)
// some0: Option.Some[Int] = Some(2)
val some1: Option.Some[Int] = Option.Some(3)
// some1: Option.Some[Int] = Some(3)

다른 모든 enum처럼 ADT도 메서드를 정의할 수 있어요. 예를 들어 isDefined 메서드와 컴패니언 객체의 Option(...) 생성자를 가진 Option을 다시 봐요.

enum Option[+T]:
  case Some(x: T)
  case None

  def isDefined: Boolean = this match
    case None => false
    case _    => true

object Option:

  def apply[T >: Null](x: T): Option[T] =
    if x == null then None else Some(x)

end Option

열거형(enumeration)과 ADT는 서로 다른 두 개념으로 소개되곤 해요. 하지만 둘은 같은 문법 구성 요소를 공유하므로, 단순히 같은 스펙트럼의 양 끝으로 볼 수 있고 하이브리드를 만드는 것도 전혀 가능해요. 예를 들어 아래 코드는 Color를 세 개의 enum 값으로 구현하거나, RGB 값을 받는 매개변수화된 케이스로 구현해요.

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

enum의 파라미터 변성 (Parameter Variance of Enums)

기본적으로 타입 파라미터를 가진 enum의 매개변수화된 케이스는 부모의 타입 파라미터와 변성 표기를 그대로 복사해요. 평소처럼 타입 파라미터가 변성일 때는 신중하게 쓰는 게 중요합니다. 아래에서 보여줄게요.

다음 View enum은 반변 타입 파라미터 T와 단일 케이스 Refl을 가지며, 타입 T를 자기 자신으로 매핑하는 함수를 나타내요:

enum View[-T]:
  case Refl(f: T => T)

Refl의 정의는 틀렸어요. 반변 타입 T를 함수 타입의 공변 결과 위치에 사용하기 때문이에요. 그래서 다음 에러가 발생합니다:

-- Error: View.scala:2:12 --------
2 |   case Refl(f: T => T)
  |             ^^^^^^^^^
  |contravariant type T occurs in covariant position in type T => T of value f
  |enum case Refl requires explicit declaration of type T to resolve this issue.

Refl이 명시적 파라미터를 선언하지 않았기 때문에, 컴파일러 관점에서는 다음과 같이 보여요:

enum View[-T]:
  case Refl[/*synthetic*/-T1](f: T1 => T1) extends View[T1]

컴파일러는 ViewT를 따라 Refl에 반변 타입 파라미터 T1을 추론했어요. 이제 Reflf를 올바르게 타이핑하려면 자신만의 무변동 타입 파라미터를 선언해야 한다는 게 명확해지죠. Refl에 다음처럼 바꾸면 에러가 해결됩니다:

enum View[-T]:
-  case Refl(f: T => T)
+  case Refl[R](f: R => R) extends View[R]

위에서 타입 RView의 타입 T와 다른 의미를 지닌다는 점을 강조하려고 Refl의 파라미터로 골랐는데, 이름은 무엇이든 상관없어요.

몇 가지 변경을 더 거치면 View의 더 완전한 구현이 다음과 같이 나오고, 함수 타입 T => U로 사용할 수 있어요:

enum View[-T, +U] extends (T => U):
  case Refl[R](f: R => R) extends View[R, R]

  final def apply(t: T): U = this match
    case refl: Refl[r] => refl.f(t)

enum의 문법 (Syntax of Enums)

문법 변경은 enum 정의와 enum 안의 케이스라는 두 범주로 나뉘어요. 변경 사항은 여기에 주어진 Scala 문법 기준의 델타(delta)로 아래에 명시돼요.

  • Enum 정의는 다음과 같이 정의됩니다:
TmplDef   ::=  `enum' EnumDef
EnumDef   ::=  id ClassConstr [`extends' [ConstrApps]] EnumBody
EnumBody  ::=  [nl] ‘{’ [SelfType] EnumStat {semi EnumStat} ‘}’
EnumStat  ::=  TemplateStat
            |  {Annotation [nl]} {Modifier} EnumCase
  • Enum의 케이스는 다음과 같이 정의됩니다:
EnumCase  ::=  `case' (id ClassConstr [`extends' ConstrApps]] | ids)

참고 (Reference)

자세한 내용은 Issue #1970을 참고하세요.

더 알아보기 (Learn more)