대수적 데이터 타입

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

대수적 데이터 타입(ADT)은 enum 구조로 만들 수 있어요. 그래서 ADT를 살펴보기 전에 먼저 **열거형(enumerations)**을 간단히 훑어볼게요.

출처: Scala 3 Book

본문

열거형 (Enumerations)

열거형은 이름 붙은 값들의 집합으로 이루어진 타입을 정의하는 데 써요:

enum Color:
  case Red, Green, Blue

이것은 다음과 같은 축약형으로 볼 수 있어요:

enum Color:
  case Red   extends Color
  case Green extends Color
  case Blue  extends Color

파라미터 (Parameters)

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

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

이렇게 하면 각각의 서로 다른 변형(variant)이 rgb라는 값 멤버를 갖게 되고, 각자 대응하는 값이 할당돼요:

println(Color.Green.rgb) // prints 65280

사용자 정의 (Custom Definitions)

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)
  // 5 or 6 more planets ...

클래스나 케이스 클래스처럼, enum에 대해 컴패니언 객체도 정의할 수 있어요:

object Planet:
  def main(args: Array[String]) =
    val earthWeight = args(0).toDouble
    val mass = earthWeight / Earth.surfaceGravity
    for (p <- values)
      println(s"Your weight on $p is ${p.surfaceWeight(mass)}")

대수적 데이터 타입 (ADTs)

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

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

이 예제는 공변(covariant) 타입 파라미터 T를 가진 Option enum을 만드는데, SomeNone 두 케이스로 이루어져 있어요. Some은 값 파라미터 x로 파라미터화되었는데, 이는 Option을 확장하는 케이스 클래스를 쓰는 축약형이에요. None은 파라미터화되지 않았으므로 일반 enum 값으로 취급됩니다.

이전 예제에서 생략했던 extends 절을 명시적으로 쓸 수도 있어요:

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

일반 enum 값과 마찬가지로, enum의 케이스들은 enum의 컴패니언 객체에 정의돼요. 그래서 Option.SomeOption.None으로 참조됩니다(import로 정의를 "끌어내지" 않는 한):

scala> Option.Some("hello")
val res1: t2.Option[String] = Some(hello)

scala> Option.None
val res2: t2.Option[Nothing] = None

다른 enum 사용처럼 ADT도 추가 메서드를 정의할 수 있어요. 예를 들어 여기 Option이 다시 나오는데, isDefined 메서드와 컴패니언 객체의 Option(...) 생성자를 갖고 있어요:

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

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

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

열거형과 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)

재귀적 열거형 (Recursive Enumerations)

지금까지 정의한 열거형들은 값이나 케이스 클래스의 다양한 변형들로 이루어져 있었어요. 열거형은 재귀적일 수도 있는데, 아래 자연수를 인코딩하는 예제에서 볼 수 있어요:

enum Nat:
  case Zero
  case Succ(n: Nat)

예를 들어 Succ(Succ(Zero)) 값은 단항(1진) 인코딩에서 숫자 2를 나타내요. 리스트도 매우 비슷한 방식으로 정의할 수 있어요:

enum List[+A]:
  case Nil
  case Cons(head: A, tail: List[A])

일반화된 대수적 데이터 타입 (GADTs)

열거형의 위 표기법은 매우 간결해서 데이터 타입을 모델링하는 완벽한 출발점이 돼요. 우리는 항상 더 명시적일 수 있기 때문에, 훨씬 더 강력한 타입도 표현할 수 있는데, 그것이 바로 **일반화된 대수적 데이터 타입(GADT)**이에요.

여기 타입 파라미터(T)가 상자에 저장된 내용물을 지정하는 GADT 예제가 있어요:

enum Box[T](contents: T):
  case IntBox(n: Int) extends Box[Int](n)
  case BoolBox(b: Boolean) extends Box[Boolean](b)

특정 생성자(IntBox 또는 BoolBox)에 대한 패턴 매칭은 타입 정보를 되찾아줘요:

def extract[T](b: Box[T]): T = b match
  case IntBox(n)  => n + 1
  case BoolBox(b) => !b

첫 번째 케이스에서 Int를 반환하는 것이 안전한 이유는, 패턴 매칭을 통해 입력이 IntBox라는 것을 알기 때문이에요.

열거형의 디슈가링 (Desugaring Enumerations)

개념적으로 enum은 sealed class와 그 컴패니언 객체를 정의하는 것으로 생각할 수 있어요. 위 Color enum의 디슈가링을 살펴볼게요:

sealed abstract class Color(val rgb: Int) extends scala.reflect.Enum
object Color:
  case object Red extends Color(0xFF0000) { def ordinal = 0 }
  case object Green extends Color(0x00FF00) { def ordinal = 1 }
  case object Blue extends Color(0x0000FF) { def ordinal = 2 }
  case class Mix(mix: Int) extends Color(mix) { def ordinal = 3 }

  def fromOrdinal(ordinal: Int): Color = ordinal match
    case 0 => Red
    case 1 => Green
    case 2 => Blue
    case _ => throw new NoSuchElementException(ordinal.toString)

참고로 위 디슈가링은 단순화된 것이고, 몇몇 세부사항은 의도적으로 생략했어요.

enum을 다른 구조로 수동으로 인코딩할 수도 있지만, 열거형을 쓰는 편이 더 간결하고 fromOrdinal 메서드 같은 몇 가지 추가 유틸리티도 함께 얻게 돼요.

더 알아보기

  • enum과 케이스 클래스, 컴패니언 객체에 대한 더 자세한 설명은 "Domain Modeling" 섹션에서 다룹니다.
  • 공변성(covariance) 등 타입 파라미터에 대한 내용은 "Variance" 페이지를 확인해 보세요.