learn

Rust enum — sum type으로서의 열거형

핵심

struct가 *"여러 필드를 동시에 가진다"*라면, enum은 "여러 후보 중 정확히 하나다". type theory에서는 이를 product type vs sum type이라 부르고, 둘이 모여 ADT(algebraic data type)를 이룬다.

struct Rectangle { width: u32, height: u32 }   // width AND height (product)
enum IpAddrKind { V4, V6 }                      // V4 OR V6 (sum)

이름의 유래는 가능한 값의 개수다. struct가 가질 수 있는 값의 수는 (필드들의 가능한 값 수의) 곱(product), enum은 합(sum)이다. 이 차이가 모든 특징의 출발점.

가장 단순한 형태

enum IpAddrKind {
    V4,
    V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

fn route(ip_kind: IpAddrKind) { /* ... */ }

이 단계까지는 다른 언어 enum과 비슷하다. variant는 enum 이름 아래로 namespacing되고(IpAddrKind::V4), 둘 다 같은 타입이라 한 함수가 양쪽을 모두 받는다. 다만 C enum이 정수 상수 alias인 데 비해, Rust enum은 이 단계부터 진짜 별개 타입이라 정수와 호환되지 않는다.

variant에 데이터 붙이기 — 결정적 차이

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

각 variant가 자기만의 데이터를 들고 다닌다. C enum, 보통의 Java enum, TypeScript enum에서는 불가능한 표현이다. 그리고 variant마다 데이터의 모양이 달라도 된다:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

variant 형태는 더 풍부해질 수 있다.

enum Message {
    Quit,                       // 데이터 없음
    Move { x: i32, y: i32 },    // struct 같은 명명 필드
    Write(String),              // 튜플 형태
    ChangeColor(i32, i32, i32),
}

OOP 상속 계층의 환원

Java/C++에서 abstract class Shape + Circle/Rectangle/Triangle 자식 클래스로 표현하던 패턴이 Rust에선 enum 하나로 환원된다.

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

상속 없이도 동등한 표현력이 나오며, 함수가 Shape를 받으면 어떤 variant든 다 받는다. 분기 처리는 다음 절의 match가 visitor 패턴보다 훨씬 직접적으로 풀어준다. 직전 is-rust-object-oriented에서 본 "Rust가 상속을 거부하고 trait + enum으로 풀었다"의 후반부가 여기서 구체화된다.

enum에도 method 정의 가능

impl Message {
    fn call(&self) { /* self는 호출된 variant */ }
}

let m = Message::Write(String::from("hello"));
m.call();

struct의 impl 블록 (what-are-methods-and-associated-functions)이 enum에도 동일하게 작동한다. self가 어떤 variant인지를 method 안에서 match로 분기하는 식으로 사용한다.

자동으로 생기는 생성자 함수

variant 이름이 자동으로 함수처럼 작동한다.

let f: fn(String) -> IpAddr = IpAddr::V4;
let v = f(String::from("..."));  // IpAddr::V4("...")와 동일

iter().map(IpAddr::V4) 같은 함수형 패턴에 자연스럽게 들어맞는다.

다른 언어와의 매칭

언어sum type 표현
C, 옛 Java, TypeScriptenum (named ints) — variant가 데이터 못 가짐
Java 17+sealed class + record — Rust enum과 거의 동등
Swiftenum (associated values) — Rust와 동일
Kotlinsealed class — Rust enum과 동등
Haskell, ML`data IpAddr = V4 String
C++std::variant<T, U, ...> — 비슷하지만 문법이 어색

Rust enum은 ML/Haskell 계열 ADT를 시스템 언어 문법으로 가져온 형태이며, 2010년대 이후 거의 모든 신규 언어가 이 패턴을 채택했다. Java 17의 sealed class 도입도 같은 흐름.

결론

Rust enum은 단순한 named integer constants가 아니라 각 variant가 서로 다른 모양의 데이터를 들고 다닐 수 있는 sum type이다. struct와 짝을 이뤄 ADT를 구성하며, OOP에서 상속 계층으로 표현하던 도메인 모델링을 — 상속 없이 — 단일 타입으로 깔끔하게 풀어낸다. variant 분기는 다음 절의 match로 처리한다.

관련: option-as-null-alternative