Попробуйте [Результат], IO [Результат], либо [Ошибка, Результат], который я должен использовать в конце
Я хотел бы знать, какая должна быть подпись моих методов, чтобы я обрабатывал различные виды сбоев элегантно.
Этот вопрос является как-то сводкой многих вопросов, которые я уже имел об обработке ошибок в Scala. Здесь вы можете найти несколько вопросов:
В настоящее время я понимаю следующее:
- Либо можно использовать в качестве оболочки результата для вызова метода, который может выйти из строя
- Попытка - это правильный биаизм. Либо, когда отказ является нефатальным исключением.
- IO (scalaz) помогает создавать чистые методы, которые обрабатывают операции ввода-вывода
- Все 3 легко можно использовать для понимания
- Все 3 не легко смешиваются в целях понимания из-за несовместимых методов FlatMap.
- В функциональных языках мы обычно не бросаем исключения, если они не являются фатальными.
- Мы должны бросить исключения для действительно исключительных условий. Я думаю, это подход Try.
- Создание Throwables имеет производительность для JVM и не предназначено для использования для управления бизнес-потоком.
Уровень репозитория
Теперь, пожалуйста, подумайте, что у меня есть UserRepository
. UserRepository
хранит пользователей и определяет метод findById
. Возможны следующие сбои:
- Фатальный сбой (
OutOfMemoryError
)
- Ошибка ввода-вывода, поскольку база данных недоступна/доступна для чтения
Кроме того, пользователь может отсутствовать, что приведет к результату Option[User]
Используя реализацию репозитория JDBC, можно выбросить SQL, нефатальные исключения (нарушение ограничений или другие), чтобы иметь смысл использовать Try.
Поскольку мы имеем дело с операциями ввода-вывода, тогда монада IO также имеет смысл, если мы хотим чистых функций.
Таким образом, тип результата может быть:
-
Try[Option[User]]
-
IO[Option[User]]
- что-то еще?
Сервисный уровень
Теперь давайте представим бизнес-уровень UserService
, который предоставляет некоторый метод updateUserName(id,newUserName)
, который использует ранее определенный findById
репозитория.
Возможны следующие сбои:
- Все сбои репозитория распространяются на уровень службы
- Бизнес-ошибка: не удается обновить имя пользователя пользователя, который не существует
- Бизнес-ошибка: новое имя пользователя слишком короткое.
Тогда тип результата может быть:
-
Try[Either[BusinessError,User]]
-
IO[Either[BusinessError,User]]
- что-то еще?
BusinessError здесь не является Throwable, потому что это не исключительный сбой.
Использование понятий
Я бы хотел использовать методы for-comprehensions для объединения вызовов методов.
Мы не можем легко смешивать разные монады для понимания, поэтому, я думаю, у меня должен быть какой-то тип равномерного возвращения для всех моих операций?
Мне просто интересно, как вам добиться успеха в ваших реальных приложениях Scala, чтобы продолжать использовать для понимания, когда могут произойти различные сбои.
В настоящее время для понимания работает отлично для меня, используя службы и репозитории, которые все возвращают Either[Error,Result]
, но все разные виды сбоев растапливаются вместе, и это становится своего рода хаккой для обработки этих сбоев.
Вы определяете неявные преобразования между различными типами монад, чтобы иметь возможность использовать для-понимания?
Вы определяете свои собственные монады для обработки отказов?
Кстати, я скоро использую асинхронный драйвер ввода-вывода.
Поэтому, я думаю, мой тип возврата может быть еще сложнее: IO[Future[Either[BusinessError,User]]]
Любые советы приветствуются, потому что я действительно не знаю, что использовать, в то время как мое приложение не причудливо: это просто API, где я должен иметь возможность различать бизнес-ошибки, которые могут быть показаны клиенту сбоку и технические ошибки. Я пытаюсь найти элегантное и чистое решение.
Ответы
Ответ 1
Это то, что для Monad трансформатор Scalaz EitherT
. Стопка IO[Either[E, A]]
эквивалентна EitherT[IO, E, A]
, за исключением того, что первая должна обрабатываться как несколько монад в последовательности, тогда как последняя автоматически представляет собой одну монаду, которая добавляет возможности Either
к базовой монаде IO
. Вы также можете использовать EitherT[Future, E, A]
для добавления несинхронной обработки ошибок в асинхронные операции.
Монад-трансформаторы в целом являются ответом на необходимость объединения нескольких монадов в одно единственное for
-понимание и/или монадическое действие.
EDIT:
Предполагаю, что вы используете Scalaz версии 7.0.0.
Чтобы использовать монадный трансформатор EitherT
поверх монады IO
, сначала нужно импортировать соответствующие части Scalaz:
import scalaz._, scalaz.effect._
Вам также необходимо определить типы ошибок: RepositoryError
, BusinessError
и т.д. Это работает как обычно. Вам просто нужно убедиться, что вы можете, например, преобразовать любой RepositoryError
в BusinessError
, а затем сопоставить шаблон, чтобы восстановить точный тип ошибки.
Затем подписи ваших методов становятся:
def findById(id: ID): EitherT[IO, RepositoryError, User]
def updateUserName(id: ID, newUserName: String): EitherT[IO, BusinessError, User]
В каждом из ваших методов вы можете использовать стек монады EitherT
-and- IO
как единую, унифицированную монаду, доступную в for
-постижениях, как обычно. EitherT
позаботится о том, чтобы направить основную монаду (в данном случае IO
) на все вычисление, а также обрабатывать ошибки, как обычно делает Either
(за исключением уже смещенных по умолчанию по умолчанию, поэтому у вас нет постоянно обращаться со всем обычным мусором .right
). Если вы хотите выполнить операцию IO
, все, что вам нужно сделать, это поднять ее в объединенный стек монады с помощью метода экземпляра liftIO
на IO
.
В качестве побочного примечания при работе таким образом функции в объекте EitherT
companion могут быть очень полезными.