대수적 데이터 타입
대수적 데이터 타입 (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을 만드는데, Some과 None 두 케이스로 이루어져 있어요. 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.Some과 Option.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" 페이지를 확인해 보세요.