问题
We use Twitter futures (as part of the Finagle stack) and I don't like the concept of using (business) exceptions to control the flow of our application, because exceptions don't show up in method signatures.
So I had the idea to use Future[Either[A,B]] as a replacement.
But I have some problems in using for comprehensions over futures with this concept:
E.g. we have a repository method:
def getUserCredentialsByNickname(nickname: String): Future[Either[EntityNotFound, UserCredentials]]
and a handler method which uses this repo and does some other checks and also creates a token
def process(request: LoginRequest): Future[Either[Failure, Login]] = {
for {
credentialsEither <- userRepository.getUserCredentialsByNickname(request.username)
...several other calls/checks which should 'interrupt' this for comprehension
token <- determineToken(credentials)
} yield token
The calls in the for comprehension after the getUserCredentialsByNickname(..) should only be executed if this call returns a Right[UserCredentials], but also the detailed error information from each returned Either should be returned from the handler.
回答1:
So now I've tried to use Scalaz Either (which is a right biased Either compared to the neutral scala Either) and the Monad Transformer EitherT and it seems it does exactly what I want. Thanks to Huw and especially Lars Hupel for hinting me in the right direction.
Here is working a sample for Twitter futures and Scalaz Either and EitherT:
import com.twitter.util.{Await, Future}
import scalaz.{Monad, Functor, EitherT, \/}
import scalaz.syntax.ToIdOps
object EitherTest extends App with ToIdOps{
// make Twitter futures work with EitherT
implicit val FutureFunctor = new Functor[Future] {
def map[A, B](a: Future[A])(f: A => B): Future[B] = a map f
}
implicit val FutureMonad = new Monad[Future] {
def point[A](a: => A): Future[A] = Future(a)
def bind[A, B](fa: Future[A])(f: (A) => Future[B]): Future[B] = fa flatMap f
}
// The example begins here:
case class InvalidInfo(error: String)
case class Response(msg: String)
class ComponentA {
def foo(fail: Boolean): Future[\/[InvalidInfo, Response]] = {
if(fail) Future(InvalidInfo("Error A").left) else Future(Response("ComponentA Success").right)
}
}
class ComponentB {
def bar(fail: Boolean): Future[\/[InvalidInfo, Response]] = {
if(fail) Future(InvalidInfo("Error B").left) else Future(Response("ComponentB Success").right)
}
}
val a = new ComponentA
val b = new ComponentB
val result = for {
resultA <- EitherT(a.foo(false))
resultB <- EitherT(b.bar(false))
} yield (resultA, resultB)
println(Await.result(result.run))
}
回答2:
You could extend the Future class by implicitly adding a method that handles Either
, instead of having to match it by yourself every time:
implicit class EitherHandlingFuture[Exception, Value](future: Future[Either[Exception, Value]]) {
def mp[Return](fn: Value => Return) = {
future.map { eth: Either[Exception, Value] =>
eth match {
case Left(ex: Exception) => { print("logging the exception") /* handle or rethrow */ }
case Right(res: Value) => fn(res)
}
}
}
}
Then, this would be possible:
def someComputation: Future[Either[Exception, Int]] = Future.value(Right(3))
someComputation mp { res: Int =>
println(res)
}
Note that the snippet above doesn't play with for
comprehensions, because to support them, it would be necessary to fully implement map/flatMap. For that, you'd probably want to subclass Future
.
来源:https://stackoverflow.com/questions/18828067/combining-futures-twitter-and-either-in-scala