如何在Scala中将以下flatMap / map片段转换为全面理解?

时间:2019-12-30 13:01:20

标签: scala for-comprehension

在Scala中理解以下代码段的最佳形式(无衬里/无回叫,更少样板)是什么?

val result = emailTakenFuture.flatMap { emailTaken =>
  if (emailTaken) {
    Future.successful(SignUpResult.EmailAlreadyTaken)
  } else {
    usernameTakenFuture.flatMap { usernameTaken =>
      if (usernameTaken) {
        Future.successful(SignUpResult.UsernameAlreadyTaken)
      } else {
        nextIdFuture.flatMap { userId =>
          storeUserFuture(userId).map(user => SignUpResult.Success(user))
        }
      }
    }
  }
}

3 个答案:

答案 0 :(得分:2)

只有最后else之后的部分非常适合理解:

for {
  userId <- nextIdFuture
  user <- storeUserFuture(userId)
} yield SignUpResult.Success(user)

其余的我只写一个辅助函数:

def condFlatMap[T](future: Future[Boolean], ifTrue: T)(ifFalse: => Future[T]): Future[T] = 
  future.flatMap(x => if (x) Future.successful(ifTrue) else ifFalse)

val result = 
  condFlatMap(emailTakenFuture, SignUpResult.EmailAlreadyTaken) {
    condFlatMap(usernameTakenFuture, SignUpResult.UsernameAlreadyTaken) {
      for {
        userId <- nextIdFuture
        user <- storeUserFuture(userId)
      } yield SignUpResult.Success(user)
    }
  }

(未经测试,但应该大致正确)

答案 1 :(得分:2)

您可能需要考虑将中间结果包装在Throwables中。然后,您以后可以恢复自己的未来-仅对那些异常进行模式匹配。

我添加了“样板”以使示例可编译:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext

implicit val executionContext: ExecutionContext = ExecutionContext.global

case class User()

def emailTakenFuture: Future[Boolean] = ???
def usernameTakenFuture: Future[Boolean] = ???
def nextIdFuture: Future[String] = ???
def storeUserFuture(userId: String): Future[User]

为简洁起见,我扩展了Throwable。您可能需要将注册结果包装在自定义异常中,以免将它们与SignupResult类型一起公开。

trait SignUpResult

case object SignUpResult {
  case object EmailAlreadyTaken extends Throwable with SignUpResult
  case object UsernameAlreadyTaken extends Throwable with SignUpResult
  case class Success(user: User) extends SignUpResult
}

val result: Future[SignUpResult] = {
  (for {
    emailTaken <- emailTakenFuture
    _ <- if (emailTaken) Future.failed(SignUpResult.EmailAlreadyTaken) else Future.successful(Unit)
    userNameTaken <- usernameTakenFuture
    _ <- if (userNameTaken) Future.failed(SignUpResult.UsernameAlreadyTaken) else Future.successful(Unit)
    userId <- nextIdFuture
    user <- storeUserFuture(userId)
  } yield SignUpResult.Success(user)).recoverWith {
    case (SignUpResult.EmailAlreadyTaken) => Future.successful(SignUpResult.EmailAlreadyTaken)
    case (SignUpResult.UsernameAlreadyTaken) => Future.successful(SignUpResult.UsernameAlreadyTaken)
  }
}

答案 2 :(得分:1)

考虑EitherT重构

type SignupResult[A] = EitherT[Future, SignupError, A]

其中SignupError是以下ADT:

sealed trait SignupError
case object EmailAlreadyTaken    extends SignupError
case object UsernameAlreadyTaken extends SignupError
case object UserIdError          extends SignupError
case object UserCreationError    extends SignupError

然后给出以下方法签名

def validateEmail(email: String): SignupResult[Unit] = ???
def validateUsername(username: String): SignupResult[Unit] = ???
def nextId(): SignupResult[String] = ???
def storeUser(userId: String): SignupResult[User] = ???

流量趋于平坦,理解力强

(for {
  _      <- validateEmail("picard@starfleet.org")
  _      <- validateUsername("picard")
  userId <- nextId()
  user   <- storeUser(userId)
} yield user).value

这是工作中的example

import cats.data.EitherT
import cats.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

object EitherTExample extends App {
  sealed trait SignupError
  case object EmailAlreadyTaken extends SignupError
  case object UsernameAlreadyTaken extends SignupError
  case object UserIdError extends SignupError
  case object UserCreationError extends SignupError

  final case class User(id: String, username: String)

  type SignupResult[A] = EitherT[Future, SignupError, A]

  def validateEmail(email: String): SignupResult[Unit] = EitherT.rightT(())
  def validateUsername(username: String): SignupResult[Unit] = EitherT.leftT(UsernameAlreadyTaken)
  def nextId(): SignupResult[String] = EitherT.rightT("42424242")
  def storeUser(userId: String): SignupResult[User] = EitherT.rightT(User("42424242", "picard"))

  val result: Future[Either[SignupError, User]] =
    (for {
      _      <- validateEmail("picard@starfleet.org")
      _      <- validateUsername("picard")
      userId <- nextId()
      user   <- storeUser(userId)
    } yield user).value

  result.map(v => println(v))
}

输出

Left(UsernameAlreadyTaken)

请注意,我们如何true / false代替Right / Left来进行验证。