Scala-way处理for-comprehensions的条件?

And*_*hev 7 scala exception-handling future

我正在尝试创建一个整洁的结构,以便理解基于未来的业务逻辑.这是一个示例,其中包含基于异常处理的工作示例:

(for {
  // find the user by id, findUser(id) returns Future[Option[User]]
  userOpt <- userDao.findUser(userId)        
  _ = if (!userOpt.isDefined) throw new EntityNotFoundException(classOf[User], userId)

  user = userOpt.get       

  // authenticate it, authenticate(user) returns Future[AuthResult]
  authResult <- userDao.authenticate(user)   
  _ = if (!authResult.ok) throw new AuthFailedException(userId)

  // find the good owned by the user, findGood(id) returns Future[Option[Good]]
  goodOpt <- goodDao.findGood(goodId)        
  _ = if (!good.isDefined) throw new EntityNotFoundException(classOf[Good], goodId)

  good = goodOpt.get        

  // check ownership for the user, checkOwnership(user, good) returns Future[Boolean]
  ownership <- goodDao.checkOwnership(user, good)
  if (!ownership) throw new OwnershipException(user, good)

  _ <- goodDao.remove(good) 
} yield {
  renderJson(Map(
    "success" -> true
  ))
})
.recover {
  case ex: EntityNotFoundException =>
    /// ... handle error cases ...
    renderJson(Map(
        "success" -> false,
        "error" -> "Your blahblahblah was not found in our database"
    ))
  case ex: AuthFailedException =>
    /// ... handle error cases ...
  case ex: OwnershipException =>
    /// ... handle error cases ...
}
Run Code Online (Sandbox Code Playgroud)

但是,这可能被视为处理事物的非功能性或非Scala方式.有一个更好的方法吗?

请注意,这些错误来自不同的来源 - 有些属于业务级别('检查所有权'),有些属于控制器级别('授权'),有些属于数据库级别('未找到实体').因此,从单个常见错误类型派生它们时的方法可能不起作用.

DCK*_*ing 8

不要对预期行为使用异常.

它在Java中并不好用,而且它在Scala中真的不太好用.请参阅此问题以获取有关为何应避免使用常规控制流异常的更多信息.Scala非常适合避免使用异常:您可以使用Eithers.

诀窍是定义您可能遇到的一些故障,并将您的Options转换为Either包含这些故障的s.

// Failures.scala
object Failures {
   sealed trait Failure

   // Four types of possible failures here
   case object UserNotFound extends Failure
   case object NotAuthenticated extends Failure
   case object GoodNotFound extends Failure
   case object NoOwnership extends Failure
   // Put other errors here...

   // Converts options into Eithers for you
   implicit class opt2either[A](opt: Option[A]) {
      def withFailure(f: Failure) = opt.fold(Left(f))(a => Right(a))
   }
}
Run Code Online (Sandbox Code Playgroud)

使用这些帮助程序,您可以使您的理解可读且无异常:

import Failures._    

// Helper function to make ownership checking more readable in the for comprehension
def checkGood(user: User, good: Good) = {
    if(checkOwnership(user, good))
        Right(good)
    else
        Left(NoOwnership)
}

// First create the JSON
val resultFuture: Future[Either[Failure, JsResult]] = for {
    userRes <- userDao.findUser(userId)
    user    <- userRes.withFailure(UserNotFound).right
    authRes <- userDao.authenticate(user)
    auth    <- authRes.withFailure(NotAuthenticated).right
    goodRes <- goodDao.findGood(goodId)
    good    <- goodRes.withFailure(GoodNotFound).right
    checkedGood <- checkGood(user, good).right
} yield renderJson(Map("success" -> true)))

// Check result and handle any failures 
resultFuture.map { result =>
    result match {
        case Right(json) => json // serve json
        case Left(failure) => failure match {
            case UserNotFound => // Handle errors
            case NotAuthenticated =>
            case GoodNotFound =>
            case NoOwnership =>
            case _ =>
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


cmb*_*ter 7

你可以稍微清理一下for comprehension,看起来像这样:

  for {
    user <- findUser(userId)
    authResult <- authUser(user)      
    good <- findGood(goodId)
    _ <- checkOwnership(user, good)    
    _ <- goodDao.remove(good) 
  } yield {
    renderJson(Map(
      "success" -> true
    ))
  }
Run Code Online (Sandbox Code Playgroud)

假设这些方法:

def findUser(id:Long) = find(id, userDao.findUser)
def findGood(id:Long) = find(id, goodDao.findGood)

def find[T:ClassTag](id:Long, f:Long => Future[Option[T]]) = {
  f(id).flatMap{
    case None => Future.failed(new EntityNotFoundException(implicitly[ClassTag[T]].runtimeClass, id))
    case Some(entity) => Future.successful(entity)
  }    
}

def authUser(user:User) = {
  userDao.authenticate(user).flatMap{
    case result if result.ok => Future.failed(new AuthFailedException(userId))
    case result => Future.successful(result)
  }    
}

def checkOwnership(user:User, good:Good):Future[Boolean] = {
  val someCondition = true //real logic for ownership check goes here
  if (someCondition) Future.successful(true)
  else Future.failed(new OwnershipException(user, good))
}
Run Code Online (Sandbox Code Playgroud)

这里的想法是利用flatMap转之类的东西Options时返回包裹在Futures转换失败的Future时候,他们是None.对于comp来说,有很多方法可以清理它,这是一种可行的方法.


Arn*_*sen 5

中心的挑战是,for-comprehensions一次只能在一个monad上工作,在这种情况下它是Futuremonad,短路未来调用序列的唯一方法是将来失败.这是有效的,因为for-comprehension中的后续调用只是mapflatmap调用,并且失败的map/ 的行为是返回该未来而不执行提供的主体(即被调用的函数).flatmapFuture

您要实现的目标是根据某些条件对工作流程进行简短的调整,而不是通过未来的失败来实现.这可以通过将结果包装在另一个容器中来完成,让我们调用它Result[A],这使得理解成为一种类型Future[Result[A]].Result将包含结果值,或者是终止结果.挑战是如何:

  • 提供后续函数调用先前非终止所包含的值 Result
  • 如果Result正在终止,则阻止评估后续函数调用

map/flatmap似乎是考生做这些类型的组合物,但我们必须手动调用它们,因为只有map/flatmap换理解可以评估是一个导致Future[Result[A]].

Result 可以定义为:

trait Result[+A] {

  // the intermediate Result
  def value: A

  // convert this result into a final result based on another result
  def given[B](other: Result[B]): Result[A] = other match {
    case x: Terminator => x
    case v => this
  }

  // replace the value of this result with the provided one
  def apply[B](v: B): Result[B]

  // replace the current result with one based on function call
  def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]]

  // create a new result using the value of both
  def combine[B](other: Result[B]): Result[(A, B)] = other match {
    case x: Terminator => x
    case b => Successful((value, b.value))
  }
}
Run Code Online (Sandbox Code Playgroud)

对于每个调用,该操作实际上是一个潜在的操作,因为调用它或使用终止结果,将简单地保持终止结果.请注意,TerminatorResult[Nothing]因为它永远不会包含一个值,任何Result[+A]可以是一个Result[Nothing].

终止结果定义为:

sealed trait Terminator extends Result[Nothing] {
  val value = throw new IllegalStateException()

  // The terminator will always short-circuit and return itself as
  // the success rather than execute the provided block, thus
  // propagating the terminating result
  def flatMap[A2 >: Nothing, B](f: A2 => Future[Result[B]]): Future[Result[B]] =
    Future.successful(this)

  // if we apply just a value to a Terminator the result is always the Terminator
  def apply[B](v: B): Result[B] = this

  // this apply is a convenience function for returning this terminator
  // or a successful value if the input has some value
  def apply[A](opt: Option[A]) = opt match {
    case None => this
    case Some(v) => Successful[A](v)
  }

  // this apply is a convenience function for returning this terminator or
  // a UnitResult
  def apply(bool: Boolean): Result[Unit] = if (bool) UnitResult else this
}
Run Code Online (Sandbox Code Playgroud)

终止结果可以[A]在我们已经满足终止条件时短路调用需要值的函数.

非终止结果定义为:

trait SuccessfulResult[+A] extends Result[A] {

  def apply[B](v: B): Result[B] = Successful(v)

  def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]] = f(value)
}

case class Successful[+A](value: A) extends SuccessfulResult[A]

case object UnitResult extends SuccessfulResult[Unit] {
  val value = {}
}
Run Code Online (Sandbox Code Playgroud)

非终止结果使得可以[A]向函数提供包含的值.为了更好的衡量,我还预定了一个UnitResult纯粹副作用的函数,比如goodDao.removeGood.

现在让我们定义你的好处,但是终止条件:

case object UserNotFound extends Terminator

case object NotAuthenticated extends Terminator

case object GoodNotFound extends Terminator

case object NoOwnership extends Terminator
Run Code Online (Sandbox Code Playgroud)

现在我们有了创建您正在寻找的工作流程的工具.每个用于完成的函数都需要一个Future[Result[A]]在右侧返回a的函数,Result[A]在左侧生成一个函数.在flatMapResult[A]使得可以调用(或短路),需要一个函数[A]作为输入,然后,我们可以map将其结果到一个新的Result:

def renderJson(data: Map[Any, Any]): JsResult = ???
def renderError(message: String): JsResult = ???

val resultFuture = for {

  // apply UserNotFound to the Option to conver it into Result[User] or UserNotFound
  userResult <- userDao.findUser(userId).map(UserNotFound(_))

  // apply NotAuthenticated to AuthResult.ok to create a UnitResult or NotAuthenticated
  authResult <- userResult.flatMap(user => userDao.authenticate(user).map(x => NotAuthenticated(x.ok)))

  goodResult <- authResult.flatMap(_ => goodDao.findGood(goodId).map(GoodNotFound(_)))

  // combine user and good, so we can feed it into checkOwnership
  comboResult = userResult.combine(goodResult)

  ownershipResult <- goodResult.flatMap { case (user, good) => goodDao.checkOwnership(user, good).map(NoOwnership(_))}

  // in order to call removeGood with a good value, we take the original
  // good result and potentially convert it to a Terminator based on
  // ownershipResult via .given
  _ <- goodResult.given(ownershipResult).flatMap(good => goodDao.removeGood(good).map(x => UnitResult))
} yield {

  // ownership was the last result we cared about, so we apply the output
  // to it to create a Future[Result[JsResult]] or some Terminator
  ownershipResult(renderJson(Map(
    "success" -> true
  )))
}

// now we can map Result into its value or some other value based on the Terminator
val jsFuture = resultFuture.map {
  case UserNotFound => renderError("User not found")
  case NotAuthenticated => renderError("User not authenticated")
  case GoodNotFound => renderError("Good not found")
  case NoOwnership => renderError("No ownership")
  case x => x.value
}
Run Code Online (Sandbox Code Playgroud)

我知道这是一个很大的设置,但至少这种Result类型可以用于具有终止条件的任何Future 理解.