Когда пора генерировать исключение в функциональном программировании

Скажем, у меня есть веб-приложение с UserController. Клиент отправляет запрос HTTP POST, который должен быть обработан контроллером. Однако сначала необходимо проанализировать предоставленный json для UserDTO. По этой причине существует UserDTOConverter с методом toDTO(json): User.

Учитывая, что я ценю методы функционального программирования за их преимущества ссылочной прозрачности и чистой функции, возникает вопрос. Каков наилучший подход к работе с, возможно, неразборчивым json? Первый вариант — создать исключение и обработать его в глобальном обработчике ошибок. Недопустимый json означает, что что-то пошло не так (например, хакер), и эта ошибка неисправима, следовательно, исключение актуально (даже при условии FP). Второй вариант — вернуть Maybe<User> вместо User. Затем в контроллере мы можем на основе типа возвращаемого значения возвращать ответ HTTP об успешном завершении или отказе. В конечном итоге оба подхода приводят к одной и той же реакции неудачи/успеха, но какой из них предпочтительнее?

Другой пример. Скажем, у меня есть веб-приложение, которому нужно получить некоторые данные из удаленного репозитория UserRepository. Из UserController репозиторий называется getUser(userId): User. Опять же, как лучше всего справиться с ошибкой возможного несуществующего пользователя с предоставленным идентификатором? Вместо возвращения User я снова могу вернуться Maybe<User>. Затем в контроллере этот результат можно обработать, например, возвращая «204 No Content». Или я мог бы бросить исключение. Код остается ссылочно прозрачным, так как снова я позволяю исключению всплывать вплоть до глобального обработчика ошибок (без блоков try catch).

Принимая во внимание, что в первом примере я бы больше склонялся к созданию исключения, во втором я бы предпочел вернуть Maybe. Исключения приводят к более чистому коду, поскольку кодовая база не загромождена вездесущими Eithers, Maybes, пустыми коллекциями и т. д. Однако возврат таких структур данных обеспечивает явность вызовов, а imo приводит к лучшей обнаруживаемости ошибки.

Есть ли место для исключений в функциональном программировании? Какова самая большая ошибка использования исключений вместо возврата Maybes или Eithers? Имеет ли смысл создавать исключения в приложении на основе FP? Если да, то есть ли для этого эмпирическое правило?

Maybe/Either — это два типа, которые кодируют понятие короткого замыкания. В зависимости от использования это также может означать исключение, которое всегда перехватывается в вашей программе. Разница в том, что императивные исключения — это уникальная языковая конструкция, специально предназначенная для кодирования ожидаемых исключений, тогда как Maybe/Either — это размеченные типы объединения первоклассных значений. Первый референциально непрозрачен, второй прозрачен. Последний гораздо более общий, потому что короткое замыкание не обязательно означает исключение, но также недетерминизм или отсутствие результата.
Iven Marquardt 18.11.2022 11:50
Знайте свои исключения!
Знайте свои исключения!
В Java исключение - это событие, возникающее во время выполнения программы, которое нарушает нормальный ход выполнения инструкций программы. Когда...
Управление ответами api для исключений на Symfony с помощью KernelEvents
Управление ответами api для исключений на Symfony с помощью KernelEvents
Много раз при создании api нам нужно возвращать клиентам разные ответы в зависимости от возникшего исключения.
2
1
74
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы спрашиваете о нескольких разных сценариях, и я постараюсь рассмотреть каждый из них.

Вход

Первый вопрос касается преобразования UserDTO (или вообще любых входных данных) в более сильное представление (User). Такое преобразование обычно является автономным (не имеет внешних зависимостей), поэтому может быть реализовано как чистая функция . Лучший способ просмотреть такую ​​функцию — это парсер.

Обычно синтаксические анализаторы возвращают значения Either (также известные как Result), например Either<Error, User>. Однако монада Someone является короткозамкнутой, а это означает, что если есть более одной проблемы с вводом, только первая проблема будет сообщена как ошибка.

При проверке ввода часто требуется собрать и вернуть список всех проблем, чтобы клиент мог исправить все проблемы и повторить попытку. Монада не может этого сделать, а аппликативный функтор может. В общем, я считаю, что валидация — это решаемая проблема.

Таким образом, вам нужно смоделировать валидацию как тип, изомоморфный Either, но имеющий другое поведение аппликативного функтора и не имеющий монадного интерфейса. По приведенным выше ссылкам уже показаны некоторые примеры, но вот реалистичный пример C#: Пример аппликативной проверки резервирования на C#.

Доступ к данным

Доступ к данным отличается, потому что вы ожидаете, что данные уже действительны. Однако чтение из хранилища данных может «пойти не так» по двум разным причинам:

  • Данных там нет
  • Хранилище данных недоступно

Первая проблема (запрос отсутствующих данных) может возникнуть по разным причинам, и обычно ее целесообразно планировать. Таким образом, запрос к базе данных для пользователя должен возвращать Maybe<User>, указывая клиенту, что он должен быть готов обрабатывать оба случая: пользователь есть или пользователя нет.

Другая проблема заключается в том, что хранилище данных иногда может быть недоступно. Это может быть вызвано сетевым разделом или проблемами на сервере базы данных. В таких случаях клиентский код обычно мало что может с этим поделать, поэтому я обычно не беспокоюсь о явном моделировании таких сценариев. Другими словами, я бы позволил реализации генерировать исключение, а клиентский код, как правило, не перехватывал бы его (за исключением регистрации в журнале).

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

Ответ принят как подходящий

TL;DR

Если в кодовой базе есть «может быть» или «любое», у вас обычно есть проблема с вводом-выводом, беспорядочно смешанным с бизнес-логикой. Это не станет лучше, если вы замените их исключениями (или наоборот).


Марк Зееманн уже дал хороший ответ, но я хотел бы остановиться на одном конкретном моменте:

Исключения приводят к более чистому коду, поскольку кодовая база не загромождена вездесущими «либо», «может быть», пустыми коллекциями и т. д.

Это не обязательно верно. Любая часть.

Проблема с исключениями

Проблема с исключениями заключается в том, что они обходят нормальный поток управления, что может затруднить анализ кода. Это кажется настолько очевидным, что едва ли заслуживает упоминания, пока вы не столкнетесь с ошибкой, вызванной 20 вызовами в глубине стека вызовов, где неясно, что вызвало ошибку в первую очередь: даже если трассировка стека может указывать на вас к точной строке в коде, вам может быть очень трудно определить состояние приложения, вызвавшее ошибку. Тот факт, что вы можете быть недисциплинированным в отношении переходов между состояниями в императивной/процедурной программе, — это, конечно, все, что пытается исправить FP.

Может быть, может быть, нет: это может быть любой из них

У вас не должно быть вездесущих Mays/Eithers по всей кодовой базе, и по той же причине вы не должны бросать исключения волей-неволей по всей кодовой базе: это слишком усложняет код. У вас должны быть файлы, которые являются точками входа в систему, и эти файлы, связанные с вводом-выводом, будут заполнены «может быть» или «любым», но затем они должны делегировать обычным функциям, которые либо поднимаются, либо отправляются через какой-то другой механизм в зависимости от языка. (вы не указываете язык). По крайней мере, языки с опционными типами почти всегда поддерживают первоклассные функции, вы всегда можете использовать обратный вызов.

Это похоже на тестируемость как показатель качества кода: если ваш код сложно тестировать, вероятно, у него есть структурные проблемы. Если ваша кодовая база полна «может быть/любое» в каждом файле, возможно, у нее есть структурные проблемы.

Другие вопросы по теме