Скажем, у меня есть веб-приложение с 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. Исключения приводят к более чистому коду, поскольку кодовая база не загромождена вездесущими Either
s, Maybe
s, пустыми коллекциями и т. д. Однако возврат таких структур данных обеспечивает явность вызовов, а imo приводит к лучшей обнаруживаемости ошибки.
Есть ли место для исключений в функциональном программировании? Какова самая большая ошибка использования исключений вместо возврата Maybe
s или Either
s? Имеет ли смысл создавать исключения в приложении на основе FP? Если да, то есть ли для этого эмпирическое правило?
Вы спрашиваете о нескольких разных сценариях, и я постараюсь рассмотреть каждый из них.
Первый вопрос касается преобразования UserDTO
(или вообще любых входных данных) в более сильное представление (User
). Такое преобразование обычно является автономным (не имеет внешних зависимостей), поэтому может быть реализовано как чистая функция . Лучший способ просмотреть такую функцию — это парсер.
Обычно синтаксические анализаторы возвращают значения Either
(также известные как Result
), например Either<Error, User>
. Однако монада Someone является короткозамкнутой, а это означает, что если есть более одной проблемы с вводом, только первая проблема будет сообщена как ошибка.
При проверке ввода часто требуется собрать и вернуть список всех проблем, чтобы клиент мог исправить все проблемы и повторить попытку. Монада не может этого сделать, а аппликативный функтор может. В общем, я считаю, что валидация — это решаемая проблема.
Таким образом, вам нужно смоделировать валидацию как тип, изомоморфный Either
, но имеющий другое поведение аппликативного функтора и не имеющий монадного интерфейса. По приведенным выше ссылкам уже показаны некоторые примеры, но вот реалистичный пример C#: Пример аппликативной проверки резервирования на C#.
Доступ к данным отличается, потому что вы ожидаете, что данные уже действительны. Однако чтение из хранилища данных может «пойти не так» по двум разным причинам:
Первая проблема (запрос отсутствующих данных) может возникнуть по разным причинам, и обычно ее целесообразно планировать. Таким образом, запрос к базе данных для пользователя должен возвращать Maybe<User>
, указывая клиенту, что он должен быть готов обрабатывать оба случая: пользователь есть или пользователя нет.
Другая проблема заключается в том, что хранилище данных иногда может быть недоступно. Это может быть вызвано сетевым разделом или проблемами на сервере базы данных. В таких случаях клиентский код обычно мало что может с этим поделать, поэтому я обычно не беспокоюсь о явном моделировании таких сценариев. Другими словами, я бы позволил реализации генерировать исключение, а клиентский код, как правило, не перехватывал бы его (за исключением регистрации в журнале).
Короче говоря, выбрасывайте только те исключения, которые вряд ли будут обработаны. Используйте типы сумм для ожидаемых ошибок.
Если в кодовой базе есть «может быть» или «любое», у вас обычно есть проблема с вводом-выводом, беспорядочно смешанным с бизнес-логикой. Это не станет лучше, если вы замените их исключениями (или наоборот).
Марк Зееманн уже дал хороший ответ, но я хотел бы остановиться на одном конкретном моменте:
Исключения приводят к более чистому коду, поскольку кодовая база не загромождена вездесущими «либо», «может быть», пустыми коллекциями и т. д.
Это не обязательно верно. Любая часть.
Проблема с исключениями заключается в том, что они обходят нормальный поток управления, что может затруднить анализ кода. Это кажется настолько очевидным, что едва ли заслуживает упоминания, пока вы не столкнетесь с ошибкой, вызванной 20 вызовами в глубине стека вызовов, где неясно, что вызвало ошибку в первую очередь: даже если трассировка стека может указывать на вас к точной строке в коде, вам может быть очень трудно определить состояние приложения, вызвавшее ошибку. Тот факт, что вы можете быть недисциплинированным в отношении переходов между состояниями в императивной/процедурной программе, — это, конечно, все, что пытается исправить FP.
У вас не должно быть вездесущих Mays/Eithers по всей кодовой базе, и по той же причине вы не должны бросать исключения волей-неволей по всей кодовой базе: это слишком усложняет код. У вас должны быть файлы, которые являются точками входа в систему, и эти файлы, связанные с вводом-выводом, будут заполнены «может быть» или «любым», но затем они должны делегировать обычным функциям, которые либо поднимаются, либо отправляются через какой-то другой механизм в зависимости от языка. (вы не указываете язык). По крайней мере, языки с опционными типами почти всегда поддерживают первоклассные функции, вы всегда можете использовать обратный вызов.
Это похоже на тестируемость как показатель качества кода: если ваш код сложно тестировать, вероятно, у него есть структурные проблемы. Если ваша кодовая база полна «может быть/любое» в каждом файле, возможно, у нее есть структурные проблемы.
Maybe
/Either
— это два типа, которые кодируют понятие короткого замыкания. В зависимости от использования это также может означать исключение, которое всегда перехватывается в вашей программе. Разница в том, что императивные исключения — это уникальная языковая конструкция, специально предназначенная для кодирования ожидаемых исключений, тогда какMaybe
/Either
— это размеченные типы объединения первоклассных значений. Первый референциально непрозрачен, второй прозрачен. Последний гораздо более общий, потому что короткое замыкание не обязательно означает исключение, но также недетерминизм или отсутствие результата.