Обработка ошибок: result против исключений
Учимся обрабатывать сбои двумя способами OCaml — типобезопасным result для ожидаемых ошибок и исключениями для исключительных.
result — вариантный тип
Ok of 'a | Error of 'e, который делает возможный сбой частью возвращаемого значения и типа функции.
OCaml даёт два механизма обработки ошибок, и зрелый код использует оба осознанно. Тип result хорош, когда сбой — ожидаемый исход, а исключения — для действительно исключительных ситуаций или прерывания глубоко вложенного вычисления.
Тип result
type ('a, 'e) result = Ok of 'a | Error of 'e
let parse_age s =
match int_of_string_opt s with
| None -> Error "не число"
| Some n when n < 0 -> Error "возраст отрицательный"
| Some n -> Ok n
let show = function
| Ok n -> Printf.sprintf "возраст: %d" n
| Error msg -> "ошибка: " ^ msg
Тип parse_age — string -> (int, string) result. Преимущество перед исключениями: ошибка видна в типе и не может быть незаметно «потеряна».
Исключения
exception Empty_list
let head = function
| [] -> raise Empty_list
| x :: _ -> x
let safe_head lst =
try Some (head lst)
with Empty_list -> None
Стандартная библиотека сама бросает исключения: List.hd [] — Failure, выход за границы — Invalid_argument, деление на ноль — Division_by_zero.
Когда что выбирать
| Ситуация | Инструмент |
| ожидаемый сбой (плохой ввод) | result или option |
| нарушение инварианта | исключение |
| быстрое прерывание рекурсии | исключение (эффективно) |
| публичный API с явными ошибками | result |
Сообщество OCaml тяготеет к result для предсказуемых ошибок: явный тип лучше документирует контракт. Но исключения остаются — они эффективнее для управления потоком.
Как работает под капотом
Исключения в OCaml реализованы очень эффективно: raise разворачивает стек до ближайшего try почти без накладных расходов, поэтому их можно использовать даже для управления потоком. Тип result не требует разворачивания стека: это обычное значение, передаваемое вверх по цепочке. Цена за явность — нужно «протаскивать» result через каждый уровень, для чего есть Result.bind и Result.map. Это та же монадическая идея, что у option, роднящая OCaml с Haskell, F# и Scala.
Частые ошибки
- Использовать исключения для рутинных ошибок. Тогда контракт функции скрыт; лучше
result. - Ловить все исключения подряд (
try ... with _ -> ...). Это маскирует баги. - Игнорировать
Errorпри разбореresult. Не сводите всё кOkс заглушкой.
Итоги
result(Ok/Error) делает ожидаемый сбой частью типа функции — ошибку нельзя незаметно потерять.- Исключения (
exception/raise/try ... with) эффективны для исключительных ситуаций. - Идиома:
resultдля предсказуемых ошибок, исключения — для нарушений инвариантов.