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,
}

各行に 1 つのバリアントを並べます。名前は自由、コロンのあとの型がそのバリアントが運ぶペイロードです。ペイロードが不要なバリアントは 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 にマッチする

match が choice 値を読む方法です。形は次の通り。

match value {
    .variantA => binding { /* value が variantA のときの本体、ペイロードは `binding` に */ }
    .variantB => binding { /* value が variantB のときの本体 */ }
}

公式 Zero リポジトリの動作例——Run をクリックすると .ok アームが発火します。

match を文字通り読むと、「result が保持するバリアントに応じて、マッチするアームを実行し、ペイロードを選んだ名前に束縛する」。.ok アームでは valuei32 ペイロードです。.err アームでは messageString ペイロードです。各アームは別のスコープで、バインディングはそのアーム本体の内側でだけ見えます。

網羅性

ここが matchif/else if 連鎖に対する大きな見返りです——コンパイラーがすべてのバリアントにアームがあるかを検証します.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    => _ { /* 束縛するものはなし */ }
}

「ペイロードを無視する」の正確な綴りは pre-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、Shape、Enum の使い分け

shapeenum からのおさらいです。

  • Shape——複数のフィールドを持つレコードで、すべて一緒に存在する。
  • Enum——N 個のラベルのひとつで、追加データなし。
  • Choice——N 個のバリアントのひとつで、それぞれがペイロードを持つ。

現実のプログラムにおけるほとんどのデータモデルは、この 3 つの組み合わせです。「これは andor か、それとも データ付きの or か?」と問うところから始まる明快さは、小さな言語で作業する過小評価されたメリットのひとつです。

次回: 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でコードを学ぼう

始める