Menu

Zero choice와 match: 태그된 합타입과 패턴 매칭

Zero에서 choice로 태그된 합타입을 선언하고 match로 변종을 망라적으로 분기하는 방법 — Zero 식의 합타입과 패턴 매칭입니다.

이 페이지에는 실행 가능한 에디터가 있습니다 — 편집하고 실행하면 결과를 바로 볼 수 있습니다.

Zero 스타일의 태그된 합타입

choice는 여러 명명된 변종 중 하나의 값이 되는 타입을 선언하며, 각 변종은 자신의 페이로드를 담습니다.

choice Result {
    ok:  i32,
    err: String,
}

Result 값은 _둘 중 하나_입니다. i32를 담은 ok 이거나 String을 담은 err. 둘 다인 경우도, 둘 다 아닌 경우도 없습니다. 타입 시스템이 그것을 보장하고, match가 그 위에서 편하게 동작하게 만들어 줍니다.

다른 언어에서 "태그된 합타입", "합타입", "구분 가능한 합타입", "대수 데이터 타입"이라고 부르는 것과 같은 아이디어입니다. Zero는 choice라고 적고 문법을 작게 유지합니다.

choice 선언하기

choice Name {
    variantA: PayloadTypeA,
    variantB: PayloadTypeB,
}

각 줄이 변종 하나를 적습니다. 이름은 마음대로 짓고, 콜론 뒤의 타입은 그 변종이 담는 페이로드입니다. 페이로드가 필요 없는 변종은 Void를 사용합니다.

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

Token.eof는 유용한 페이로드가 없는 변종입니다(페이로드 타입은 Void). 종료자 스타일의 경우에 유용해요.

choice 값 만들기

타입 이름, 변종, 페이로드 순으로 적어 값을 만듭니다.

let success = Result.ok(42)
let failure = Result.err("validation failed")

페이로드 타입은 변종의 선언된 페이로드와 맞아야 합니다. Result.ok("hello")oki32를 기대하기 때문에 컴파일 에러입니다.

여기서도 타입 추론이 동작합니다. 오른쪽이 타입을 완전히 결정한다면 let success = Result.ok(42)라고 적어도 바인딩 타입이 Result가 됩니다. 바인딩 지점에 타입을 문서화하고 싶다면 명시적으로 적어도 좋아요.

let success: Result = Result.ok(42)

choice 매칭하기

matchchoice 값을 읽는 방법입니다. 모양은 이렇습니다.

match value {
    .variantA => binding { /* value가 variantA일 때 본문, 페이로드는 `binding`에 */ }
    .variantB => binding { /* value가 variantB일 때 본문 */ }
}

공식 Zero 저장소에서 가져온 예제 — Run을 눌러 .ok 분기가 실행되는 걸 확인해 보세요.

match를 문자 그대로 읽어 보세요. "result가 어떤 변종을 담고 있는지에 따라, 그에 맞는 분기를 실행하고 페이로드를 선택된 이름에 바인딩한다." .ok 분기에서 valuei32 페이로드입니다. .err 분기에서 messageString 페이로드고요. 각 분기는 별도의 스코프이며, 바인딩은 자기 본문 안에서만 보입니다.

망라성

if/else if 체인 대비 match의 큰 장점이 바로 이것입니다. 컴파일러가 모든 변종에 분기가 있는지 검증해 줍니다. .err를 잊어버리면, 런타임에 기본 분기로 떨어지는 게 아니라 컴파일 에러가 납니다.

{
    "code": "MAT001",
    "message": "match is not exhaustive: missing variant 'err'",
    "line": 9
}

(에러 코드는 예시 수준이고, 원칙이 곧 계약입니다.)

choice에 새 변종을 추가하면 — 예를 들어 Result.timeout: Void — 코드베이스의 모든 Result에 대한 match가 그 경우를 처리할 때까지 컴파일 에러가 됩니다. 귀찮은 일이 아니라 기능입니다. 컴파일러가 새 경우에 주의를 줘야 할 자리를 정확히 알려주는 거니까요.

페이로드가 필요 없을 때

변종의 페이로드가 Void이거나 이 분기에서 그 페이로드에 관심이 없다면, 바인딩은 무시해도 됩니다 — 하지만 망라성을 만족시키려면 분기 자체는 적어야 합니다.

match token {
    .word   => w { /* w 사용 */ }
    .number => n { /* n 사용 */ }
    .eof    => _ { /* 바인딩할 게 없음 */ }
}

"페이로드 무시"에 대한 정확한 표기는 1.0 이전 Zero에서 바뀔 수 있습니다(_를 보거나 바인딩을 그냥 생략하게 될 수 있어요). 개념적인 포인트 — 페이로드가 있든 없든 모든 변종이 분기를 받는다 — 가 안정된 부분입니다.

자주 쓰는 패턴

Result 스타일의 에러 타입

공식 저장소가 사용하는 바로 그 예제입니다.

choice Result {
    ok:  i32,
    err: String,
}

값으로 성공하거나 메시지로 실패할 수 있는 함수는 Result를 반환합니다. 호출자는 패턴 매칭으로 값이나 메시지를 꺼내요. Zero의 raises/check 시스템은 실패할 수 있는 연산의 _전파_를 다루고, Result는 성공-또는-실패 값을 데이터로 쥐고 있고 싶을 때 유용합니다.

파서 토큰

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

토크나이저는 Token 스트림을 만들어 냅니다. 각 소비자는 변종에 매치해서 무엇을 할지 결정합니다 — 단어 출력, 숫자 합산, eof에서 종료 등.

상태 기계

choice State {
    waiting:    Void,
    processing: i32,
    done:       String,
}

processing은 현재 작업 ID를, done은 최종 결과를 담습니다. 각 전이가 새 State 값이고, shape에 흩어진 가변 필드가 아닙니다.

choice + 제네릭

choiceshape처럼 제네릭이 될 수 있습니다.

choice Maybe<T> {
    some: T,
    none: Void,
}

Maybe<i32>는 "선택적 정수"입니다. Maybe<String>은 "선택적 문자열"이고요. 이 같은 패턴이 Zero 표준 라이브러리에 등장하고, null 센티널 값보다 훨씬 잘 어울려요. 타입에 대해 match를 적는 순간 .none 경우를 잊을 수가 없거든요.

choice vs. shape vs. enum 언제 쓸까

shapeenum 요약:

  • shape — 여러 필드가 모두 함께 있는 레코드.
  • enum — N개의 라벨 중 하나, 추가 데이터 없음.
  • choice — N개의 변종 중 하나, 각각 페이로드를 담음.

실제 프로그램의 데이터 모델 대부분은 이 셋의 조합입니다. "이건 _그리고_인가, _또는_인가, 아니면 _데이터를 가진 또는_인가?"에서 출발하는 명확함은 작은 언어로 일할 때의 과소평가된 장점 중 하나예요.

다음 글: World 능력

choicematch는 Zero의 데이터 측면을 다뤘습니다. 다음 챕터는 효과입니다. Zero 프로그램이 바깥 세상과 어떻게 상호작용하는지요. 모든 I/O를 통제하는 객체인 World 능력에서 시작합니다.

자주 묻는 질문

Zero에서 choice가 뭔가요?

choice는 Zero의 태그된 합타입입니다. 여러 명명된 변종 중 하나의 값이 되고, 각 변종이 자신만의 페이로드 타입을 가집니다. 예: choice Result { ok: i32, err: String }. Result 값은 i32를 담은 ok이거나 String을 담은 err이에요. Result.ok(42)Result.err("bad") 식으로 만듭니다.

Zero에서 match는 어떻게 동작하나요?

match value { .variantA => binding { ...본문 } .variantB => binding { ...본문 } }value가 어떤 변종을 담고 있는지에 따라 분기합니다. 각 분기는 변종을 패턴 매칭하고, 페이로드 바인딩 이름을 지정하고, 본문을 실행해요. 컴파일러는 모든 변종을 다뤘는지 검증합니다. 망라성이야말로 if/else if보다 나은 핵심 이점입니다.

choice 값은 어떻게 만드나요?

타입과 변종 이름을 적고 페이로드를 전달해서 만듭니다: let r: Result = Result.ok(42)let r = Result.err("validation failed") 식으로요. 페이로드 타입은 변종의 선언된 페이로드와 맞아야 합니다. 잘못된 타입을 넘기면 컴파일 에러가 납니다.

choice와 enum은 어떻게 다른가요?

enum 변종은 페이로드 없는 그냥 라벨입니다. choice 변종은 각각 선언된 타입의 값을 담아요. 경우 중 하나에 데이터(에러 메시지, 성공 결과, 파싱된 토큰)를 붙여야 한다면 choice를 쓰세요. 경우가 순전한 라벨이면 enum을 쓰면 됩니다.

choice 위에서 if-else보다 match를 선호하는 이유는?

match는 본질적으로 망라적이에요. 컴파일러가 모든 변종이 처리됐는지 검사하기 때문에, 나중에 새 변종을 추가하면 그 타입을 분기하는 모든 자리를 강제로 갱신하게 만듭니다. if/else if 체인은 조용히 흘려보내며, 빠진 경우가 운영 환경에서 버그로 드러나기 전까지 숨어 있어요.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기