Tagged Unions auf Zero-Art
choice deklariert einen Typ, dessen Wert eine von mehreren benannten Varianten ist, wobei jede Variante ihren eigenen Payload trägt:
choice Result {
ok: i32,
err: String,
}
Ein Result-Wert ist entweder ein ok, das ein i32 trägt, oder ein err, das einen String trägt. Nie beides, nie keines. Das Typsystem garantiert das, und match macht es bequem, darauf zu reagieren.
Das ist dieselbe Idee, die andere Sprachen „Tagged Union", „Sum Type", „Discriminated Union" oder „algebraischer Datentyp" nennen. Zero schreibt sie choice und hält die Grammatik klein.
Eine Choice deklarieren
choice Name {
variantA: PayloadTypA,
variantB: PayloadTypB,
}
Jede Zeile listet eine Variante. Der Name gehört dir; der Typ nach dem Doppelpunkt ist der Payload, den diese Variante trägt. Eine Variante, die keinen Payload braucht, verwendet Void:
choice Token {
word: String,
number: i32,
eof: Void,
}
Token.eof ist eine Variante ohne nützlichen Payload (Payload-Typ ist Void) – praktisch für Terminator-artige Fälle.
Einen Choice-Wert konstruieren
Bau einen Wert, indem du den Typ, dann die Variante benennst und den Payload übergibst:
let success = Result.ok(42)
let failure = Result.err("validation failed")
Der Payload-Typ muss zum deklarierten Payload der Variante passen. Result.ok("hello") wäre ein Compile-Fehler, weil ok einen i32 erwartet.
Typinferenz funktioniert auch hier. Wenn die rechte Seite den Typ vollständig festlegt, kannst du let success = Result.ok(42) schreiben und der Typ des Bindings ist Result. Den Typ explizit zu annotieren ist okay, wenn du ihn an der Bindungsstelle dokumentieren willst:
let success: Result = Result.ok(42)
Eine Choice matchen
match ist der Weg, einen choice-Wert zu lesen. Die Form:
match value {
.variantA => binding { /* body, wenn value variantA ist, Payload in `binding` */ }
.variantB => binding { /* body, wenn value variantB ist */ }
}
Ein durchgespieltes Beispiel aus dem offiziellen Zero-Repo – klick Run, um den .ok-Zweig feuern zu sehen:
Lies das match wörtlich: „je nachdem, welche Variante result hält, führe den passenden Zweig aus und binde den Payload an den gewählten Namen." Im .ok-Zweig ist value der i32-Payload. Im .err-Zweig ist message der String-Payload. Jeder Zweig ist ein eigener Scope; das Binding ist nur in seinem eigenen Body sichtbar.
Erschöpfendheit
Das ist der große Vorteil von match gegenüber if/else if-Ketten: der Compiler verifiziert, dass jede Variante einen Zweig hat. Vergisst du den .err-Fall, bekommst du keinen Runtime-Fall in einen Default-Zweig – du bekommst einen Compile-Fehler:
{
"code": "MAT001",
"message": "match is not exhaustive: missing variant 'err'",
"line": 9
}
(Der Fehlercode ist beispielhaft; das Prinzip ist der Vertrag.)
Fügst du eine neue Variante zur Choice hinzu – etwa Result.timeout: Void – wird jedes match gegen Result in der Codebasis zu einem Compile-Fehler, bis du den neuen Fall behandelst. Das ist ein Feature, keine lästige Pflicht: Der Compiler sagt dir genau, wo der neue Fall Aufmerksamkeit braucht.
Wenn du den Payload nicht brauchst
Wenn der Payload einer Variante Void ist oder du dich in diesem Zweig schlicht nicht für ihn interessierst, kannst du das Binding ignorieren – du musst den Zweig aber trotzdem schreiben, um die Erschöpfendheit zu erfüllen:
match token {
.word => w { /* w nutzen */ }
.number => n { /* n nutzen */ }
.eof => _ { /* nichts zu binden */ }
}
Die genaue Schreibweise für „Payload ignorieren" kann sich in pre-1.0 Zero noch ändern (du siehst eventuell _ oder das Binding einfach weggelassen). Der konzeptuelle Punkt – jede Variante bekommt einen Zweig, mit oder ohne Payload – ist der stabile Teil.
Häufige Muster
Ein Result-artiger Fehlertyp
Genau das Beispiel, das das offizielle Repo verwendet:
choice Result {
ok: i32,
err: String,
}
Funktionen, die mit einem Wert erfolgreich sein oder mit einer Meldung fehlschlagen können, geben ein Result zurück. Aufrufer pattern-matchen, um entweder den Wert oder die Meldung herauszuziehen. Zeros raises/check-System kümmert sich um Propagierung für fehlbare Operationen; Result ist nützlich, wenn du einen Erfolg-oder-Misserfolg-Wert als Daten festhalten willst.
Ein Parser-Token
choice Token {
word: String,
number: i32,
eof: Void,
}
Ein Tokenizer erzeugt einen Strom von Tokens. Jeder Konsument matched die Variante, um zu entscheiden, was zu tun ist – das Wort drucken, die Zahl aufsummieren, bei eof beenden.
Eine State Machine
choice State {
waiting: Void,
processing: i32,
done: String,
}
processing trägt die aktuelle Task-ID; done trägt das Endergebnis. Jede Transition ist ein neuer State-Wert – keine veränderlichen Felder, verstreut in einer Shape.
Choice + Generics
choice kann genauso generisch sein wie shape:
choice Maybe<T> {
some: T,
none: Void,
}
Maybe<i32> ist „ein optionaler Integer". Maybe<String> ist „ein optionaler String". Dieses Muster taucht in Zeros Standardbibliothek auf und ist eine viel bessere Lösung als ein Null-Sentinel-Wert – sobald du gegen den Typ matchst, kannst du den .none-Fall nicht vergessen.
Wann Choice, wann Shape, wann Enum?
Kurzer Rückblick aus Shapes und Enums:
- Shape – Record mit mehreren Feldern, alle zusammen vorhanden.
- Enum – eines von N Labels, ohne extra Daten.
- Choice – eine von N Varianten, jede mit Payload.
Die meisten Datenmodelle in einem echten Programm sind irgendeine Kombination dieser drei. Die Klarheit, die daraus entsteht, mit „ist das und, oder, oder oder mit Daten?" zu beginnen, ist einer der unterschätzten Vorteile, in einer kleinen Sprache zu arbeiten.
Als Nächstes: World-Capability
choice und match decken Zeros Datenseite ab. Das nächste Kapitel dreht sich um Effekte – wie Zero-Programme mit der Außenwelt interagieren. Es beginnt mit der World-Capability, dem Objekt, das jedes Stück I/O zugänglich macht.
Häufig gestellte Fragen
Was ist eine choice in Zero?
Eine choice ist Zeros Tagged-Union-Typ – ein Wert, der eine von mehreren benannten Varianten ist, wobei jede Variante ihren eigenen Payload-Typ trägt. Beispiel: choice Result { ok: i32, err: String }. Ein Result-Wert ist entweder ein ok, das ein i32 trägt, oder ein err, das einen String trägt. Du baust eines mit Result.ok(42) oder Result.err("bad").
Wie funktioniert match in Zero?
match value { .variantA => binding { ...body } .variantB => binding { ...body } } verzweigt danach, welche Variante value hält. Jeder Zweig pattern-matched eine Variante, benennt das Payload-Binding und führt seinen Body aus. Der Compiler verifiziert, dass du jede Variante abgedeckt hast – Erschöpfendheit ist der zentrale Vorteil gegenüber if/else if.
Wie baut man einen choice-Wert?
Konstruiere ihn, indem du Typ und Variante benennst und den Payload übergibst: let r: Result = Result.ok(42) oder let r = Result.err("validation failed"). Der Payload-Typ muss zum deklarierten Payload der Variante passen – einen falschen Typ zu übergeben ist ein Compile-Fehler.
Was ist der Unterschied zwischen choice und enum?
enum-Varianten sind nur Labels ohne Payload. choice-Varianten tragen je einen Wert eines deklarierten Typs. Wenn du an einen der Fälle Daten anhängen musst (eine Fehlermeldung, ein erfolgreiches Ergebnis, ein geparstes Token), nimm choice. Sind die Fälle reine Labels, nimm enum.
Warum ist match besser als if-else für choices?
match ist von Konstruktion her erschöpfend – der Compiler prüft, dass jede Variante behandelt wird, sodass eine neue Variante später jeden Ort zwingt, der über den Typ verzweigt, auf den neuesten Stand zu kommen. Eine if/else if-Kette fällt stillschweigend durch und versteckt den fehlenden Fall, bis er als Bug in Produktion auftaucht.