Tagged union в стиле Zero
choice объявляет тип, значение которого является одним из нескольких именованных вариантов, и каждый вариант несёт свой payload:
choice Result {
ok: i32,
err: String,
}
Значение Result — это либо ok с i32, либо err со String. Никогда оба сразу и никогда ни то ни другое. Это гарантирует система типов, а match делает удобным реагировать на это.
Это та же идея, которую в других языках называют tagged union, sum type, discriminated union или algebraic data type. Zero пишет это как choice и держит грамматику маленькой.
Объявление choice
choice Name {
variantA: PayloadTypeA,
variantB: PayloadTypeB,
}
Каждая строка перечисляет один вариант. Имя — ваше; тип после двоеточия — это payload, который этот вариант несёт. Вариант, которому payload не нужен, использует Void:
choice Token {
word: String,
number: i32,
eof: Void,
}
Token.eof — это вариант без полезного payload (его тип Void) — пригодится для случаев-терминаторов.
Конструирование значения choice
Постройте значение, назвав тип, затем вариант, и передав payload:
let success = Result.ok(42)
let failure = Result.err("validation failed")
Тип payload должен совпасть с объявленным у варианта. Result.ok("hello") был бы compile error, потому что ok ожидает i32.
Вывод типов тоже работает. Если правая часть полностью определяет тип, можно написать let success = Result.ok(42), и тип привязки будет Result. Аннотировать тип явно тоже нормально, если хочется задокументировать его прямо у привязки:
let success: Result = Result.ok(42)
Матчинг по choice
match — это то, как вы читаете значение choice. Форма:
match value {
.variantA => binding { /* тело, когда value это variantA, payload в `binding` */ }
.variantB => binding { /* тело, когда value это variantB */ }
}
Разбор примера из официального репозитория Zero — нажмите Run и увидите, как срабатывает arm .ok:
Прочитайте match буквально: «в зависимости от того, какой вариант лежит в result, выполни соответствующую arm и привяжи payload к выбранному имени». В arm .ok value — это payload типа i32. В arm .err message — это payload типа String. Каждая arm — отдельная область видимости; привязка видна только внутри её тела.
Исчерпывающесть
Это главный выигрыш match перед цепочкой if/else if: компилятор проверяет, что у каждого варианта есть arm. Если забыть случай .err, не получите тихий fall-through на дефолтную ветку — получите compile error:
{
"code": "MAT001",
"message": "match is not exhaustive: missing variant 'err'",
"line": 9
}
(Код ошибки иллюстративный; принцип — это контракт.)
Добавьте в choice новый вариант — скажем, Result.timeout: Void — и каждый match по Result в кодовой базе станет compile-error, пока вы не обработаете новый случай. Это фича, а не повинность: компилятор показывает вам ровно те места, где новому случаю нужно внимание.
Когда payload не нужен
Если payload варианта — Void или просто не интересен в этой arm, можно проигнорировать привязку — но arm всё равно надо написать, чтобы удовлетворить исчерпывающесть:
match token {
.word => w { /* используем w */ }
.number => n { /* используем n */ }
.eof => _ { /* нечего привязывать */ }
}
Точное написание «игнорировать payload» может эволюционировать в pre-1.0 Zero (может встретиться _ или просто пропущенная привязка). Концептуальная суть — у каждого варианта есть arm, с payload или без — это стабильная часть.
Частые паттерны
Тип ошибки в стиле Result
Это ровно тот пример, который использует официальный репозиторий:
choice Result {
ok: i32,
err: String,
}
Функции, которые могут успешно вернуть значение или упасть с сообщением, возвращают Result. Вызывающие паттерн-матчат, чтобы достать либо значение, либо сообщение. Система raises/check в Zero обрабатывает проброс для fallible-операций; Result полезен, когда вы хотите держать success-or-failure значение как данные.
Токен парсера
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-sentinel значения — забыть случай .none, когда вы match-ите по типу, попросту нельзя.
Когда choice, когда shape, когда enum
Короткое повторение из shapes и enums:
- Shape — запись с несколькими полями, все присутствуют вместе.
- Enum — одна из N меток, без дополнительных данных.
- Choice — один из N вариантов, каждый несёт payload.
Большинство моделей данных в реальной программе — это какая-то комбинация этих трёх. Ясность, которую даёт старт с вопроса «это и, или или или с данными?», — одно из недооценённых преимуществ работы в маленьком языке.
Дальше: World capability
choice и match покрывают «данные» в Zero. Следующая глава — об эффектах, о том, как программы на Zero взаимодействуют с внешним миром. Она начинается с World capability, объекта, который контролирует каждое I/O.
Часто задаваемые вопросы
Что такое choice в Zero?
choice — это tagged-union тип в Zero: значение, которое является одним из нескольких именованных вариантов, и каждый вариант несёт свой тип payload. Пример: choice Result { ok: i32, err: String }. Значение Result — это либо ok с i32, либо err со String. Сконструировать его можно через Result.ok(42) или Result.err("bad").
Как работает match в Zero?
match value { .variantA => binding { ...тело } .variantB => binding { ...тело } } ветвится по тому, какой вариант сейчас в value. Каждая arm паттерн-матчит вариант, именует привязку payload и выполняет своё тело. Компилятор проверяет, что вы покрыли каждый вариант — исчерпывающесть и есть главное преимущество перед if/else if.
Как построить значение choice?
Назовите тип и вариант и передайте payload: let r: Result = Result.ok(42) или let r = Result.err("validation failed"). Тип payload должен совпадать с объявленным у варианта — передача неправильного типа это compile error.
В чём разница между choice и enum?
Варианты enum — это просто метки без payload. У choice каждый вариант несёт значение объявленного типа. Если нужно прицепить данные к одному из случаев (сообщение об ошибке, успешный результат, разобранный токен) — используйте choice. Если случаи — просто метки, берите enum.
Почему для choice предпочтительнее match, а не if-else?
match по построению исчерпывающий — компилятор проверяет, что обработан каждый вариант, поэтому добавление нового варианта позже принуждает обновить каждое место, которое ветвится по этому типу. Цепочка if/else if молча проваливается, скрывая отсутствующий случай до тех пор, пока он не вылезет багом в проде.