【问题标题】:How to convert following flatMap/map snippet to for-comprehension in Scala?如何将以下 flatMap/map 片段转换为 Scala 中的理解?
【发布时间】:2019-12-30 13:01:20
【问题描述】:

在 Scala 中理解以下代码 sn-p 的最佳形式是什么(线性/无回调,更少样板)?

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

【问题讨论】:

    标签: scala for-comprehension


    【解决方案1】:

    只有最后一个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)
        }
      }
    

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

    【讨论】:

      【解决方案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。您可能希望将注册结果包装在自定义异常中,以免将它们与SignupResulttype 一起公开。

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

      【讨论】:

      • 这意味着,例如,EmailAlreadyTaken 是一个失败,尽管它可能是一个有效的响应。
      • 是的,我知道这一点。这就是我建议的原因:“您可能希望将注册结果包装在自定义异常中”。据我了解,OP 要求提供免回调以供理解,我举了一个例子。
      【解决方案3】:

      考虑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)
      

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

      【讨论】:

      • 这个。 EitherT 作为短路转换器,同时保持类型化的验证错误(加上未类型化的 Throwables,用于未来的实际异常错误)。或者使用 ZIO 之类的东西,这样您就可以在效果中内置输入错误(比 monad 转换器更简单,类型推断更好)。
      猜你喜欢
      • 2014-10-14
      • 2014-06-11
      • 2013-01-13
      • 1970-01-01
      • 1970-01-01
      • 2016-07-31
      • 1970-01-01
      • 2018-08-17
      • 1970-01-01
      相关资源
      最近更新 更多