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") は ok が i32 を期待するのでコンパイルエラーになります。
ここでも型推論は働きます。右辺が型を完全に確定させるなら、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 アームでは value が i32 ペイロードです。.err アームでは message が String ペイロードです。各アームは別のスコープで、バインディングはそのアーム本体の内側でだけ見えます。
網羅性
ここが match の if/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 + ジェネリクス
choice は shape と同じくジェネリックにできます。
choice Maybe<T> {
some: T,
none: Void,
}
Maybe<i32> は「オプションな整数」、Maybe<String> は「オプションな文字列」です。このパターンは Zero 標準ライブラリでも登場し、null のセンチネル値よりもずっと適切な選択です——型に対して match すれば .none ケースを忘れる方法がありません。
Choice、Shape、Enum の使い分け
- Shape——複数のフィールドを持つレコードで、すべて一緒に存在する。
- Enum——N 個のラベルのひとつで、追加データなし。
- Choice——N 個のバリアントのひとつで、それぞれがペイロードを持つ。
現実のプログラムにおけるほとんどのデータモデルは、この 3 つの組み合わせです。「これは and か or か、それとも データ付きの or か?」と問うところから始まる明快さは、小さな言語で作業する過小評価されたメリットのひとつです。
次回: World ケイパビリティ
choice と match で 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 の連鎖は静かに流れて、欠けたケースを本番のバグとして表面化するまで隠してしまいます。