ADT vs Either vs 异常

ADT vs Either vs Exceptions

因此,当前的实现使用 Twitter 的 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 块中退出。 我想避免抛出异常。解决它的一种方法是 returning Either[Error, X],另一种方法 ADT 使用 sealed trait。因此,您可以 return Left(Error)case object NoCertificate extends CertificateResponse.

之类的 ADT 而不是抛出 Exception

问题是:如果我将当前具有 throw Exception 的方法替换为 EitherADT,我能否保持现有的 for 循环完好无损?

为了完整起见,下面是我将如何编写我的 EitherADT:

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)
}

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 的东西?

请注意:我们不能像 EitherT 一样使用 cats Monad Transformer(它有 leftMap 表示退出条件),因为那不是我们在我们的库中使用的库之一堆。抱歉!

谢谢!

正如我在评论中提到的那样,我真的会研究是否有可能提供一些提供 monad 转换器的库(Scalaz 也包括一个),因为这正是它们所针对的用例。如果真的不可能,您唯一的选择是编写自己的 - 意思是,创建一些 class 可以包装您的方法输出,其中包含 mapflatMap 方法来执行您想要的操作。这对于 Either 和基于 ADT 的解决方案都是可行的。基于 Either 的看起来有点像这样:

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% 需要这种包装 class!没有办法让 return 类型 Future[ADT]Future[Either[Error, Result]] 以你想要的方式运行因为这需要改变 Future 的工作方式。)

使用上面的代码,您可以在 Result 类型上使用 for-comprehensions,如果包含 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 转换器的原因是因为它们带有 大量 实用方法和逻辑,可以让您的生活变得更加轻松,但是如果这不是一个选择,这不是一个选择。