Future[Option] in Scala for-comprehensions

前端 未结 5 1646
闹比i
闹比i 2020-12-07 19:16

I have two functions which return Futures. I\'m trying to feed a modified result from first function into the other using a for-yield comprehension.

This approach w

相关标签:
5条回答
  • 2020-12-07 19:21

    It's easier to use https://github.com/qifun/stateless-future or https://github.com/scala/async to do A-Normal-Form transform.

    0 讨论(0)
  • 2020-12-07 19:22

    This answer to a similar question about Promise[Option[A]] might help. Just substitute Future for Promise.

    I'm inferring the following types for getUserDetails and getSchool from your question:

    getUserDetails: UserID => Future[Either[??, UserDetails]]
    getSchool: SchoolID => Future[Option[School]]
    

    Since you ignore the failure value from the Either, transforming it to an Option instead, you effectively have two values of type A => Future[Option[B]].

    Once you've got a Monad instance for Future (there may be one in scalaz, or you could write your own as in the answer I linked), applying the OptionT transformer to your problem would look something like this:

    for {
      ud  <- optionT(getUserDetails(user.userID) map (_.right.toOption))
      sid <- optionT(Future.successful(ud.schoolID))
      s   <- optionT(getSchool(sid))
    } yield s
    

    Note that, to keep the types compatible, ud.schoolID is wrapped in an (already completed) Future.

    The result of this for-comprehension would have type OptionT[Future, SchoolID]. You can extract a value of type Future[Option[SchoolID]] with the transformer's run method.

    0 讨论(0)
  • 2020-12-07 19:38

    We've made small wrapper on Future[Option[T]] which acts like one monad (nobody even checked none of monad laws, but there is map, flatMap, foreach, filter and so on) - MaybeLater. It behaves much more than an async option.

    There are a lot of smelly code there, but maybe it will be usefull at least as an example. BTW: there are a lot of open questions(here for ex.)

    0 讨论(0)
  • 2020-12-07 19:45

    What behavior would you like to occur in the case that the Option[School] is None? Would you like the Future to fail? With what kind of exception? Would you like it to never complete? (That sounds like a bad idea).

    Anyways, the if clause in a for-expression desugars to a call to the filter method. The contract on Future#filteris thus:

    If the current future contains a value which satisfies the predicate, the new future will also hold that value. Otherwise, the resulting future will fail with a NoSuchElementException.

    But wait:

    scala> None.get
    java.util.NoSuchElementException: None.get
    

    As you can see, None.get returns the exact same thing.

    Thus, getting rid of the if sid.isDefined should work, and this should return a reasonable result:

      val schoolFuture = for {
        ud <- userStore.getUserDetails(user.userId)
        sid = ud.right.toOption.flatMap(_.schoolId)
        s <- schoolStore.getSchool(sid.get)
      } yield s
    

    Keep in mind that the result of schoolFuture can be in instance of scala.util.Failure[NoSuchElementException]. But you haven't described what other behavior you'd like.

    0 讨论(0)
  • 2020-12-07 19:47

    (Edited to give a correct answer!)

    The key here is that Future and Option don't compose inside for because there aren't the correct flatMap signatures. As a reminder, for desugars like so:

    for ( x0 <- c0; w1 = d1; x1 <- c1 if p1; ... ; xN <- cN) yield f
    c0.flatMap{ x0 => 
      val w1 = d1
      c1.filter(x1 => p1).flatMap{ x1 =>
        ... cN.map(xN => f) ... 
      }
    }
    

    (where any if statement throws a filter into the chain--I've given just one example--and the equals statements just set variables before the next part of the chain). Since you can only flatMap other Futures, every statement c0, c1, ... except the last had better produce a Future.

    Now, getUserDetails and getSchool both produce Futures, but sid is an Option, so we can't put it on the right-hand side of a <-. Unfortunately, there's no clean out-of-the-box way to do this. If o is an option, we can

    o.map(Future.successful).getOrElse(Future.failed(new Exception))
    

    to turn an Option into an already-completed Future. So

    for {
      ud <- userStore.getUserDetails(user.userId)  // RHS is a Future[Either[...]]
      sid = ud.right.toOption.flatMap(_.schoolId)  // RHS is an Option[Int]
      fid <- sid.map(Future.successful).getOrElse(Future.failed(new Exception))  // RHS is Future[Int]
      s <- schoolStore.getSchool(fid)
    } yield s
    

    will do the trick. Is that better than what you've got? Doubtful. But if you

    implicit class OptionIsFuture[A](val option: Option[A]) extends AnyVal {
      def future = option.map(Future.successful).getOrElse(Future.failed(new Exception))
    }
    

    then suddenly the for-comprehension looks reasonable again:

    for {
      ud <- userStore.getUserDetails(user.userId)
      sid <- ud.right.toOption.flatMap(_.schoolId).future
      s <- schoolStore.getSchool(sid)
    } yield s
    

    Is this the best way to write this code? Probably not; it relies upon converting a None into an exception simply because you don't know what else to do at that point. This is hard to work around because of the design decisions of Future; I'd suggest that your original code (which invokes a filter) is at least as good of a way to do it.

    0 讨论(0)
提交回复
热议问题