Как стать автором
Обновить

Тип данных Either как альтернатива выбрасыванию исключений

Время на прочтение8 мин
Количество просмотров10K
Всего голосов 17: ↑14 и ↓3+15
Комментарии14

Комментарии 14

Мы пишем на C# и используем такой же подход, только у нас тип называется Result<T>. В целом с ним удобнее оказалось. Вдохновлялись Railway-Oriented-Programming и библиотекой Виктора Хорикова - CSharpFunctionalExtensions

Ну почему, почему, почему опятьEither<Left,Right>, а не Result<Ok,Err>? Единственное, что тут немного радует - всё-таки догадались в котлине сделать flatMap над Either асимметричным, в отличие от скалы и тайпскрипта, что упрощает его применение для обработки ошибок, но добавляет легкий разрыв шаблона.

Э-э-э, а где в тайпскрипте вообще тип Either и его flatMap?

Возможно с TS чуть погорячился - просто свежи воспоминания, как немного полыхнуло с этой статьи. В целом да - в ядре Either нет, но зато есть fp-ts, которая насколько я заметил довольно популярная и много где рекомендуется к использованию, и там как раз есть симметричный Either.

Что вы имеете в виду под симмитричным? Что 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.

Ух ты, и правда, спасибо большое. Похоже у меня уже Indian code induced mental damage

в этом подходе не очень нравится то что теперь существует оба варианта обработки ошибок параллельно. В теории все выглядит хорошо, но я видел проекты в которых писали с таким подходом и выходила мешанина. Поскольку 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.

… Который код кидать не обязан, ибо в качестве исключения может выступать значение произвольного типа.

Это не имеет значения потому что есть catch(...), а вообще код который кидает что-то не унаследованное от std::exception можно смело приравнивать к ill-formed.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий