Попробуйте [Результат], 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 могут быть очень полезными.