Когда дело доходит до обработки ошибок, основной стратегией является прекращение всех вычислений после обнаружения первой погрешности. Обычно это достигается за счет использования исключений. Хотя этот подход работает в большинстве случаев, бывают случаи, когда он не идеален. Например, при получении запроса от пользователя предпочтительнее вернуть все ошибки сразу и позволить исправить их одним махом. В этой статье блога я рассмотрю такой сценарий и изучу конкретный пример с использованием Scala 3 и библиотеки Cats. Все примеры кода вы можете найти в данном репозитории github. Допустим, мы работаем в финансовой организации, которая получает заказы на покупку или продажу финансовых продуктов. Вот простой запрос: Тикер (ticker) — это идентификатор финансового инструмента. Количество (quantity) представляет собой желаемое число инструментов. Наконец, окончание срока (expiry) — это необязательное поле, которое сообщает нам, когда запрос считается валидным. Мы хотим проверить следующие три ограничения: Тикер не должен быть пустым Количество должно быть положительным Срок действия должен быть либо пустым, либо не позже, чем за один месяц до истечения. Давайте реализуем эти правила: По поводу истечения срока, мы, возможно, захотим ввести пользовательское перечисление, чтобы явно указать, что None означает, что срок действия запроса не истекает. После того, как мы имплементировали эти правила валидации, можно объединить их с помощью for-comprehension (синтаксис for-выражения): Теперь давайте попробуем: Пока все хорошо. Однако, если запрос содержит несколько ошибок, мы видим только первую из них, касающуюся недействительного тикера, и не понимаем, что поля количества и срока действия также недействительны. Мы видим только первую ошибку, потому что for-comprehension по своей природе является последовательным, то есть он выполняет следующую строку только в том случае, если предыдущая строка была заполнена некоторыми данными. Поэтому, если мы хотим накапливать ошибки, то не можем использовать for-comprehension или </span><span style="font-size:10pt;font-family:'Roboto Mono',monospace;color:#000000;background-color:transparent;font-weight:700;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">flatMap, нужно использовать другой метод. Но какой? На помощь приходит Typelevel Cats! Cats — это функциональная библиотека с множеством полезных типов данных и функций, которые очень хорошо дополняют стандартную библиотеку Scala. Весь код для этого раздела можно найти здесь Мы собираемся заменить for-comprehension на parMapN, который будет мерджить все ошибки вместе, если они есть. validateOrder выдает одинаковые результаты, если запрос валидный или если он содержит только одну ошибку. Единственное различие когда запрос содержит несколько ошибок: Теперь мы видим все сообщения об ошибках. К сожалению, они скомпонованы в одну строку, без знаков препинания или даже пробелов между ними. Лучшим подходом было бы поместить их в какую-либо структуру данных, например, в List. Это позволит нам в дальнейшем выбрать, каким образом отображать эти ошибки. Давайте обновим наши три функции валидации так, чтобы они возвращали List (список ошибок). Мы также делаем аналогичное обновление для validateQuantity и validateExpiry. Весь код для этого раздела вы можете найти здесь. Нам также нужно изменить тип ошибки в validateOrder на List, но в остальном тело функции остается прежним: И теперь все работает, как и ожидалось. Это можно увидеть ниже: Это позволит показать все сообщения об ошибках нашему пользователю: Это конечно неплохо, но было бы еще лучше, если бы мы могли отображать сообщения об ошибках рядом с полем, которое вызвало ошибку! Что-то наподобие того, как показано на рисунке ниже. Чтобы так сделать, нам нужно связать каждое сообщение об ошибке с идентификатором поля. Давайте попробуем это осуществить. Еще раз обновим наши функции валидации так, чтобы они паковали ошибки в Map. Аналогично для validateQuantity, validateExpiry и validateOrder. Весь код для этого раздела вы можете найти здесь. Теперь давайте запустим код с запросом, содержащим несколько ошибок: Меня впечатляет, что достаточно было изменить тип ошибки, а затем parMapN автоматически скомбинировал их вместе! Как это работает? Не перегружена ли parMapN для поддержки распространенных типов ошибок, таких как String, List и Map, или она более универсальна? parMapN — это общий метод, который работает со всеми типами ошибок до тех пор, пока мы можем выполнить сквош слияние (слияние со сжатием) его значений. На практике это означает, что тип ошибки должен иметь неявный экземпляр класса Semigroup. Это может показаться запутанным, но на самом деле определить его очень просто. Вот пример для String: Нам не нужно было имплементировать данный метод для String, List или Map, потому что библиотека Cats уже сделала это для наиболее распространенных типов стандартной библиотеки. Простой способ проверить, существует ли экземпляр, — использовать метод sumon. Как видите, Cats имплементировала экземпляр Semigroup для String и Map, но не для UUID [universally unique identifier — универсальный уникальный идентификатор], поскольку не существует эффективных способов объединить UUID вместе. Поэтому если вы хотите накапливать ошибки в пользовательском типе, вам придется определить его собственный экземпляр Semigroup. Посмотреть пример можно здесь. Мы видели, что parMapN отлично работает для объединения нескольких ошибок вместе, но что если количество валидаций неизвестно во время компиляции? Например, мы получаем батч CreateOrderRequest для обработки. Как можно проверить все запросы и вернуть все ошибки по каждому конечному пользователю? Здесь мы не можем использовать parMapN, потому что не знаем, сколько элементов находится в List. В этом случае необходимо применить другой метод из Cats: parTraverse. Давайте попробуем это сделать с двумя неверными запросами: Он действительно работает, но мы не знаем, какая ошибка соответствует какому заказу. Например, мы видим, что тикер не должен быть пустым, но не знаем, относится ли это к первому или второму запросу. Есть несколько способов решить эту проблему. Здесь я предлагаю ввести уникальный идентификатор для заказов и присвоить OrderId каждому OrderErrors: Давайте повторим тот же пример с двумя неверными запросами: Отлично, на этот раз у нас есть вся необходимая информация по отображению ошибок для нашего пользователя! Подведем итог. Мы изучили ограничения for-comprehension, когда дело доходит до накопления ошибок, и узнали о двух мощных методах из библиотеки Cats: parMapN и parTraverse. Эти методы предлагают универсальное решение, которое работает с любым типом ошибок, оснащенным экземпляром Semigroup. Мы также видели, что Cats определяет такие экземпляры для распространенных типов, таких как String, List и Map, и что при использовании пользовательского типа ошибки очень легко создать свой собственный экземпляр. Я надеюсь, что эта статья была информативной и полезной. Спасибо за прочтение! Завтра вечером пройдет открытый урок, на котором разберем практическое применение функционального дизайна в Scala. На этой встрече мы:- узнаем, зачем нам вообще может понадобиться функциональный дизайн;- сформируем представление, из каких основных компонентов он состоит;- узнаем, для решения задач в каких предметных областях он широко используется;- попрактикуемся в решении задачи, используя как декларативную, так и исполняемую модель;- выясним плюсы и минусы обеих моделей. Урок подойдет всем, кто уже знаком со Scala и хочет использовать функциональные подходы в решении задач. Записаться можно на странице курса «Scala-разработчик».  Редактор