Комментарии 14
Мы пишем на C# и используем такой же подход, только у нас тип называется Result<T>
. В целом с ним удобнее оказалось. Вдохновлялись Railway-Oriented-Programming и библиотекой Виктора Хорикова - CSharpFunctionalExtensions
Ну почему, почему, почему опятьEither<Left,Right>
, а не Result<Ok,Err>
? Единственное, что тут немного радует - всё-таки догадались в котлине сделать flatMap над Either асимметричным, в отличие от скалы и тайпскрипта, что упрощает его применение для обработки ошибок, но добавляет легкий разрыв шаблона.
Э-э-э, а где в тайпскрипте вообще тип Either и его flatMap?
Что вы имеете в виду под симмитричным? Что Left и Right равноценны? В скале это исправили годах в 2016-17 и теперь:Either is right-biased, which means that Right is assumed to be the default case to operate on. If it is Left, operations like map and flatMap return the Left value unchanged.
в этом подходе не очень нравится то что теперь существует оба варианта обработки ошибок параллельно. В теории все выглядит хорошо, но я видел проекты в которых писали с таким подходом и выходила мешанина. Поскольку Either это надстройка, а сам язык работает с исключениями, то в итоге приходится иметь дело и с исключениями, и с Either. Я видел такое и по итогу выходило что параллельно существовали одни и те же ошибки в виде исключений и объектов, и код который в try обрабатывал ошибки в ФП стиле, но и catch все равно ставил, т.к. исключения по прежнему возможны. Either выглядит как workaround, потому что нет проверяемых исключений. Плюс это возврат к старому С-стилю кодов возврата, только с небольшим синтаксическим сахарком. Насколько я понял, каких-то дополнительных статических гарантий Either не дает, и вполне можно пропустить ветку обработки ошибок точно так же как и проигнорировать исключения, или нет?
Смешались в кучу either/result/validation.
Для разумного удобства работы с either нужны проекции, иначе начинают плодиться mapLeft/mapRight и далее по тексту.
Обработка ошибок и валидация - несколько разные задачи. В первой хочется fail fast, во второй - не столько.
При обработке ошибок можно использовать either как poor man's result/try. В реальности result/try должен перехватывать не всё throwable, а только те из которых есть шанс восстановиться (и это зависит от контекста). Можете ли вы в данном случае пережить, скажем, oom или нет.
Валидация обычно пытается набрать максимально разумное количество ошибок на данном уровне. Когда вы делаете, скажем, валидацию развесистой структуры в веб-сервисе (или тупо в форме) часто полезно сообщить об ошибках во всех полях входных данных, а не только в первом попавшемся. Поэтому там любят сделать scatter-gather: на отдельных операциях перехватываем ошибки валидации, потом сворачиваем всё либо в удачный результат, либо в агрегат, описывающий проблемы. Аналогичная история с батч-запросами, шардированием, вызовом распределенных сервисов и т.п.
Думаю, код, выбрасывающий исключение всякий раз, когда произойдет что-то неожиданное, сложен для понимания, и его тяжелее поддерживать.
Это утверждение требует доказательства, без которого вся статья не имеет смысла. Мой опыт показывает обратное - только при использовании исключений код будет прозрачным, очевидным и сконцентрированным на том что мы делаем, а не на том что может пойти не так. Код обработки ошибок будет обособлен в ограниченном числе мест на верхнем уровне где мы можем сделать что-то осмысленное, а где-то вообще не нужен - консольное приложение пусть просто упадёт (в Python будет готовый трейс, в C++, ок, придётся обернуть main в try чтобы распечатать what()). В веб бэкенде исключение перехватится на уровне обработки запроса и клиенту вернётся 503. В GUI приложении я бы обернул,например, только целиком код open/save чтобы сказать "не шмогла" и продолжить работу.
Все же подходы с возвратом значений заставляют думать об обработке ошибок в каждой строке, на каждом шаге. Даже если это сделано как в rust - односимвольным оператором, мне все равно надо думать, возвращает ли эта функция голое значение или Result, хочу ли я ? или !, и что будет если сторонний код который я вызываю вызовет panic? Если функция не возвращала ошибку, но начала возвращать, мне придётся менять все её вызовы (в полюсах есть похожая проблема с noexcept, но он про оптимизацию и его можно, а в публичных интерфейсах, пожалуй, и нужно не использовать.
Но да, в редких случаях когда нужно контролировать каждый шаг алгоритма Result с монадическим интерфейсом удобен. В прочем, в языке в котором есть исключения можно без особых проблем обернуться одно в другое (может быть с точностью до производительности).
Если функция не возвращала ошибку, но начала возвращать, мне придётся менять все её вызовы
И это хорошо! В языках с исключениями добавление нового исключения, как правило, приводит к тому, что новое исключение перехватывается сильно выше уровнем, где контекст ещё не потерян и на него ещё можно отреагировать. Вы же ведь не сетуете на то, что смена возвращаемого типа функции заставляет менять места в коде, где она вызывается?
Нет, не приводит, потому что перехватывается std::exception.
Тип данных Either как альтернатива выбрасыванию исключений