Постановка
Отказ в Zero — это не отдельный параллельный поток управления. Это часть обычного потока управления, объявленная в сигнатуре функции и признанная в каждом месте вызова. Это работает за счёт двух кусочков:
raisesв сигнатуре функции — «эта функция может упасть».checkв месте вызова — «если это упадёт, провали мою функцию той же ошибкой».
Этой комбинации достаточно, чтобы выразить то, ради чего большинство языков тянется к try/catch или к типу Result.
Объявление fallible-функции
Добавьте raises после возвращаемого типа:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
Сигнатуру можно прочитать двумя способами:
- «
validateвозвращаетi32или выбрасываетInvalidInput». - «Набор возможных исходов — {
i32,InvalidInput}».
Оба прочтения корректны. Компилятор отслеживает обе возможности и требует, чтобы вызывающие что-то явное сделали с каждой.
Можно написать и голый raises:
pub fun main(world: World) -> Void raises {
check world.out.write("hello\n")
}
raises (без списка ошибок) означает «эта функция может упасть с любой ошибкой». На main это привычная форма — программа может выйти с ненулевым статусом, если что-то пошло не так, а рантайм возьмёт на себя оповещение об ошибке.
Для функций глубже в стеке вызовов предпочитайте явную форму raises { ErrorA, ErrorB }, чтобы режимы отказа были задокументированы на каждом уровне.
Выброс ошибки
Внутри fallible-функции raise выходит из функции с заданной ошибкой:
fun validate(ok: Bool) -> i32 raises { InvalidInput } {
if ok == false {
raise InvalidInput
}
return 42
}
raise InvalidInput производит ошибку InvalidInput в этой строке. Функция не продолжает дальше — управление возвращается к вызывающему, и вызывающий видит ошибку вместо i32. Clause raises перечисляет единственные типы ошибок, которые функция вправе выбрасывать; выбросить что-то не из списка — compile error.
Проброс ошибки через check
Вызывающий, дёрнувший fallible-функцию, обязан признать возможный отказ. Самое частое признание — check:
fun run() -> Void raises { InvalidInput } {
check validate(true)
}
check validate(true) делает две вещи:
- Вызывает
validate(true). - Если
validateвыбросила ошибку, пробрасывает её выше —runвыбрасывает ту же ошибку своему вызывающему.
Чтобы проброс был разрешён, run обязан в своём raises clause объявить, что может выбрасывать InvalidInput (или что-то совместимое). Компилятор это проверяет. Если бы сигнатура run говорила raises { OtherError }, проброс не скомпилировался бы, потому что InvalidInput не входит в этот набор.
Полный разобранный пример из официальных примеров языка — нажмите Run, чтобы увидеть успешный проброс:
Тип ошибки путешествует с сигнатурой функции до самого верха стека вызовов. Голый raises у main принимает всё, что может выбросить run, поэтому проброс приземляется безопасно.
Почему не try/catch?
Дисциплина дизайна за raises/check в том, что отказ никогда не невидим в месте вызова. В языке с try/catch исключение может тихо пройти через функцию, которая даже не знает, что в этом участвует — функция выглядит чистой, но какой-то глубокий вызов в её теле выбрасывает, и исключение раскручивается через неё.
Это удобно автору выбрасывающего кода. Это дорого для всех остальных:
- Читатели (и агенты) не могут по сигнатуре понять, участвует ли функция в путях отказа.
- Рефакторинг становится нервным — перенос вызова между функциями может изменить достижимые исключения.
- Восстанавливающий код живёт далеко от места, которое знает, что делать.
Zero платит цену заранее — аннотации на каждой fallible-функции и check в каждом fallible-вызове — чтобы получить свойство «режимы отказа, в которых участвует функция, видны из её сигнатуры». На это свойство могут полагаться и люди, и агенты.
Почему просто не Result<T, E>?
То же самое можно выразить через choice — тип Result<T, E> с вариантами ok и err. Zero даёт вам и этот паттерн; это полезный инструмент, когда отказ — это данные, которые вы хотите осмотреть, сохранить или передать.
raises/check добавляет синтаксическое соглашение для частого случая: «если это упадёт, провали мою функцию таким же способом». Без него каждый вызов оборачивался бы в match, который почти всегда переупаковывает ошибку в собственный Result вызывающего. check — это сокращение для этого, причём компилятор гарантирует, что проброс хорошо типизирован.
То есть:
Result<T, E>(choice) — когда вы хотите инспектировать отказ или нести его как значение.raises+check— когда вы просто хотите пробросить отказ выше по стеку.
Оба доступны; они покрывают разные эргономические нужды.
Несколько типов ошибок
Функция может выбрасывать больше одного типа ошибки:
fun parse(input: String) -> i32 raises { Empty, Malformed } {
if std.mem.len(input) == 0 {
raise Empty
}
// ... логика парсинга ...
raise Malformed
}
Вызывающий может:
- Пробросить через
check parse(input), если в его сигнатуре перечислены иEmpty, иMalformed(или их супермножество). - Обработать одну или обе ошибки явно через
matchилиtry-подобные конструкции, которые язык предоставляет для этой цели.
Точный синтаксис тонкой обработки (матчинг по конкретным типам ошибок vs. проброс остальных) — одна из поверхностей, которая может пошевелиться в pre-1.0 Zero. Контракт — каждый тип ошибки, который функция может выбросить, в её сигнатуре — это стабильная часть.
Ментальная модель
Система отказа в Zero — это строго-честная версия того, что есть у каждого императивного языка:
| Концепт | Try/Catch | Zero |
|---|---|---|
| Пометить функцию как fallible | ничего (неявно) | raises { ... } |
| Выбросить ошибку | throw e | raise E |
| Пробросить выше | поднимается невидимо | check call(...) |
| Обработать локально | try { ... } catch(e) { ... } | match по возвращённому Result или типизированная форма обработки |
Поведенческая разница маленькая. Аннотационная — большая. И это специально. Эффекты — явные. Отказы — это эффекты.
Стиль
- Используйте
checkщедро. Это правильный дефолт, когда у вас нет ничего конкретного, что делать на этом слое. - Избегайте голого
raisesна внутренних помощниках. Чем у́же набор ошибок, тем полезнее сигнатура. - Спаривайте fallible-операции с capability, которые им нужны. Функция, использующая
Worldи пишущая в stdout, почти всегда хочетraises, потому что записи могут падать.
Дальше: JSON-диагностика
raises/check работает рядом с диагностиками компилятора Zero — когда вы пишете check против функции, которая не объявляет правильные ошибки, компилятор скажет вам, что именно не так, в структурированной форме. Следующая статья — JSON-диагностика — машинно-читаемый поток, который агент читает, чтобы чинить код.
Часто задаваемые вопросы
Что означает raises в Zero?
raises в Zero?raises в сигнатуре функции объявляет, что функция может упасть. Голый raises разрешает любой тип ошибки. Конкретная форма вроде raises { InvalidInput } ограничивает функцию падением только перечисленными типами ошибок. Вызывающие обязаны признать возможность отказа — либо через check, либо через другую явную форму обработки.
Что делает оператор check?
check?check expr вычисляет expr, и если результат — ошибка, пробрасывает её выше, к вызывающему текущей функции. Думайте о нём как «выполни это, и если упадёт, провали моего вызывающего той же ошибкой». Чтобы проброс был разрешён, у вызывающего clause raises должен сам объявлять, что он может выбросить совместимую ошибку.
Как выбросить ошибку в Zero?
Используйте raise ErrorName внутри функции, в clause raises которой эта ошибка перечислена. Пример: if ok == false { raise InvalidInput }. Функция выходит в этой точке; ошибка становится исходом функции, который вызывающий может перехватить через check.
Почему в Zero нет try/catch?
Try/catch позволяет исключениям тихо проходить через функции, которые о них не знают. Дизайн Zero такой, что сигнатура каждой функции обязана признавать режимы отказа, в которых она участвует. Никакого скрытого потока управления — если функция может упасть, её сигнатура это говорит, и каждый вызывающий обязан явно признать это через check.
Может ли функция в Zero выбрасывать несколько типов ошибок?
Да — перечислите их в clause raises { ... } через запятую (или согласно текущему синтаксису Zero для наборов ошибок). Набор возможных ошибок — часть контракта функции, наравне с типами параметров и возвращаемых значений. Вызывающие могут паттерн-матчить по тому, какая ошибка выброшена, или просто пробрасывать через check.