Обработка ошибок: Result и railway-oriented programming
Ошибки как значения: тип Result делает успешный и ошибочный путь явными, без исключений.
Result — размеченное объединение с вариантами
Ok значение(успех) иError ошибка(неудача); ошибки становятся обычными значениями, которые видны в типе.
Зачем Result, а не исключения
Исключения невидимы в сигнатуре: глядя на функцию, не скажешь, что она может бросить. Result же честно объявляет в типе: «вернёт либо успех, либо ошибку». Это поощряет обработать оба исхода, как с option, но с информацией об ошибке.
let divide a b =
if b = 0 then Error "деление на ноль"
else Ok (a / b)
printfn "%A" (divide 10 2)
printfn "%A" (divide 10 0)Вывод:
Ok 5 Error "деление на ноль"
Тип divide — int -> int -> Result<int, string>: успех несёт int, ошибка — строку-сообщение.
Railway-oriented programming
Представьте вычисление как две параллельные рельсы: «успех» и «ошибка». Пока всё хорошо, поезд едет по рельсе успеха; первая ошибка переводит его на рельсу ошибки, и дальнейшие шаги пропускаются. Это и есть railway-oriented programming.
// схема двух рельсов
Ok ──> шаг1 ──> шаг2 ──> шаг3 ──> Ok
│ │ │
v v v
Error ───────────────────────> ErrorСвязующее звено — Result.bind: он применяет следующий шаг только к Ok, а Error протягивает дальше нетронутым.
Цепочка через bind
let parseAge (s: string) =
match System.Int32.TryParse s with
| true, n -> Ok n
| _ -> Error "не число"
let validate n =
if n >= 0 && n < 150 then Ok n
else Error "возраст вне диапазона"
let process s =
parseAge s
|> Result.bind validate
printfn "%A" (process "42")
printfn "%A" (process "-5")
printfn "%A" (process "abc")Вывод:
Ok 42 Error "возраст вне диапазона" Error "не число"
Как только один шаг вернул Error, следующие не выполняются — поезд ушёл на рельсу ошибки.
map для успешной рельсы
Result.map преобразует значение внутри Ok, не трогая Error — когда следующий шаг не может сам провалиться.
let doubled = Ok 21 |> Result.map (fun x -> x * 2)
printfn "%A" doubledВывод:
Ok 42
Как работает под капотом
Result — это DU type Result<'T,'E> = Ok of 'T | Error of 'E. bind внутри — простой match: для Ok x он вызывает следующую функцию, для Error e возвращает ошибку как есть. Цепочка bind и есть «рельсы»: ошибка короткозамыкает остаток. Никаких исключений и раскрутки стека — только передача значений, что делает поток управления явным и предсказуемым.
Частые ошибки
- Путать
map(следующий шаг не может провалиться) иbind(шаг сам возвращаетResult). - Смешивать исключения и
Resultбессистемно — выберите единый стиль для слоя. - Терять информацию об ошибке: тип ошибки (
'E) тоже стоит проектировать осмысленно (DU, а не голая строка).
Итоги
Resultделает ошибку значением:Ok— успех,Error— неудача с данными.- Сигнатура честно показывает возможность ошибки — её нельзя «забыть».
- Railway-oriented programming:
bindведёт по рельсе успеха, первая ошибка уводит на рельсу ошибки. mapпреобразуетOk;bind— когда шаг сам может вернутьError.