ADT против любого из исключений

Итак, текущая реализация использует твиттер Future вместе с генерацией исключений, чтобы сигнализировать о недопустимом варианте использования вместе с for-comprehensions, например:

def someMethod(a: ...): Future[X] = {
  // do something
  // if something goes wrong throw exception
  throw new Exception("Certificate not issued")
}

// in some other method, where exceptions break the loop and exit
def someOtherMethod(a: ...): Future[Y] = {
  for {
    x <- someMethod(...)
    y <- yetAnotherMethod(...) // which throws another exception
  } yield y
}

Общая идея заключается в том, что когда что-то пойдет не так, будет выброшено исключение, которое приведет к выходу из блока for-comprehension. Я хочу уйти от создания исключений. Один из способов решить эту проблему — вернуть Either[Error, X], а другой — ADT, используя sealed trait. Таким образом, вместо того, чтобы бросать Exception, вы можете вернуть Left(Error) или ADT, например case object NoCertificate extends CertificateResponse.

Вопрос: могу ли я сохранить существующие циклы for, если я заменю методы, которые в настоящее время имеют throw Exception, на Either или ADT?

Для полноты, вот как я бы закодировал свои Either и ADT:

sealed trait Error
case object CertificateError extends Error
case object SomeOtherError extends Error

def someMethod(a: ...): Future[Either[Error, CertificateResponse]] = {
  // returns Left(CertificateError) or Right(CertificateResponse)
}

OR

sealed trait CertificateResponse
case class Certificate(x509) extends CertificateResponse
case object NoCertificate extends CertificateResponse

def someMethod(a: ...): Future[CertificateResponse] = {
  // returns NoCertificate or Certificate(X509)
}

будет ли какое-либо из этих альтернативных решений (для создания исключений и нарушения ссылочной прозрачности) работать с for-comprehensions? Будет ли отрицательный ответ: Left() или NoCertificate автоматически выходить из блока for-comprehension? Если нет, то как сделать так, чтобы я мог оставить блоки for-comprehension как есть? Что-то похожее на cats EitherT's leftMap?

Обратите внимание: мы не можем использовать cats Monad Transformer, например EitherT (у которого есть leftMap, сигнализирующий об условиях выхода), так как это не одна из библиотек, которые мы используем в нашем стеке. Извини!

Спасибо!


person iyerland    schedule 15.08.2019    source источник
comment
Scalaz также включает в себя EitherT, можно ли использовать эту библиотеку? Потому что на самом деле поведение, которое вы указываете, точно дает вам использование монадного преобразователя, и если вы не можете использовать библиотеки, вам придется кодировать их вручную.   -  person Astrid    schedule 15.08.2019


Ответы (1)


Как уже упоминалось в моем комментарии, я бы действительно посмотрел, существует ли какая-то библиотека, предлагающая монадный преобразователь (Scalaz также включает один), потому что это именно тот вариант использования, для которого они предназначены. Если это действительно невозможно, ваша единственная альтернатива - написать свой собственный, то есть создать некоторый класс, который может обернуть выходные данные вашего метода, который имеет методы map и flatMap, которые делают то, что вы хотите. Это выполнимо как для решения на основе Both, так и для решения на основе ADT. Основанный на любом из них будет выглядеть примерно так:

sealed trait Error

case object CertificateError extends Error
case object SomeOtherError extends Error

case class Result[+T](value: Future[Either[Error, T]]) {
  def map[S](f: T => S)(implicit ec: ExecutionContext) : Result[S] = {
    Result(value.map(_.map(f)))
  }

  def flatMap[S](f: T => Result[S])(implicit ec: ExecutionContext) : Result[S] = {
    Result {
      value.flatMap {
        case Left(error) => Future.successful(Left(error))
        case Right(result) => f(result).value
      }
    }
  }
}

(Вам 100% нужен такой класс-оболочка! Невозможно заставить возвращаемый тип Future[ADT] или Future[Either[Error, Result]] вести себя так, как вы хотите, потому что для этого потребуется изменить способ работы Future.)

С кодом, как указано выше, вы можете использовать for-понимания над типами Result, и они автоматически завершатся, если либо их содержащий Future не удастся, либо Future преуспеет с Error, как вы указали. Глупый пример:

import ExecutionContext.Implicits.global
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._

def getZero() : Result[Int] = Result(Future.successful(Right(0)))

def error() : Result[Unit] = Result(Future.successful(Left(SomeOtherError)))

def addTwo(int: Int) : Result[Int] = Result(Future.successful(Right(2 + int)))

val result = for {
  zero <- getZero()
  _ <- error()
  two <- addTwo(zero)
} yield two

Await.result(result.value, 10.seconds) // will be `Left(SomeOtherError)`

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

person Astrid    schedule 15.08.2019