EECOLOR
EECOLOR

Reputation: 11244

Combining Futures, Eithers and Options in for comprehensions

I have a collection of methods that return different types:

Either[ErrorResponse, X]
Future[Either[ErrorResponse, X]]
Option[ErrorResponse]

These methods need the result from a previous method to perform their computation. The methods:

type Parameters = Map[String, String]

// allows me to flatmap on an either
implicit def toRightProjection[Failure, Success](e: Either[Failure, Success]) =
  e.right

// converts anything to a future
implicit def toFuture[T](t: T) =
  Future.successful(t)

// retrieves the request paramters from the given request
def requestParameters(request: RequestHeader): Either[ErrorResponse, Parameters] = ???

// retrieves the response type from the given parameters
def responseType(p: Parameters): Either[ErrorResponse, String] = ???

// retrieves the client id from the given parameters
def clientId(p: Parameters): Either[ErrorResponse, String] = ???

// retrieves the client using the given client id
def client(clientId: String): Future[Either[ErrorResponse, Client]] = ???

// validates the response type of the client
def validateResponseType(client: Client, responseType: String): Option[ErrorResponse] = ???

I can the wire them together with the following for comprehension (note that I wrote down some types to clarify the contents of specific parts of the computation).

val result: Either[ErrorResponse, Future[Either[ErrorResponse, Client]]] =
  for {
    parameters <- requestParameters(request)
    clientId <- clientId(parameters)
    responseType <- responseType(parameters)
  } yield {
    val result: Future[Either[ErrorResponse, Either[ErrorResponse, Client]]] =
      for {
        errorOrClient <- client(clientId)
        client <- errorOrClient
      } yield validateResponseType(client, responseType).toLeft(client)

    result.map(_.joinRight)
  }

val wantedResult: Future[Either[ErrorResponse, Client]] =
  result.left.map(Future successful Left(_)).merge

The above code is quite messy and I feel this can be done differently. I read about monads and monad transformers. The concept of those is very new to me and I can not get my head around it.

Most of the examples only deal with two types of results: Either[X, Y] and Future[Either[X, Y]]. I still find it very hard to bend my mind around it.

How can I write a nice and clean for comprehension that replaces the above one?

Something like this would be awesome (I am not sure if that is even possible):

val result: Future[Either[ErrorResponse, Client]] =
  for {
    parameters <- requestParameters(request)
    clientId <- clientId(parameters)
    responseType <- responseType(parameters)
    client <- client(clientId)
    _ <- validateResponseType(client, responseType)
  }

Upvotes: 11

Views: 4653

Answers (3)

flavian
flavian

Reputation: 28511

scala.util.Either is not a Monad, but the scalaz library has a great implementation.

object Test extends ToIdOps {

import scalaz.{ Monad, Functor, EitherT, \/, -\/, \/- }
import scalaz.syntax.ToIdOps

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
}
def someMethod: Future[\/[InvalidData, ValidData]] = {
   // things went well
   ValidData.right // this comes from ToIdOps
   // or something went wrong
   InvalidData.left
}
def someOtherMethod: Future[\/[InvalidData, ValidData]] // same as above
val seq = for {
  d <- EitherT(someMethod)
  y <- EitherT(someOtherMethod)
} yield { // whatever}
// you can now Await.result(seq.run, duration)
// you can map or match etc with \/- and -\/
val result = seq.run map {
   case -\/(left) => // invalid data
   case \/-(right) => // game on
}
}

Upvotes: 2

Channing Walton
Channing Walton

Reputation: 4007

OK, here is my attempt at this:

import scalaz._, Scalaz._

implicit val futureMonad = new Monad[Future] {
  override def point[A](a: ⇒ A): Future[A] = future(a)

  override def bind[A, B](fa: Future[A])(f: A ⇒ Future[B]): Future[B] =
    fa.flatMap(f)
}

import EitherT._
val result: EitherT[Future, ErrorResponse, Client] =
  for {
    parameters <- fromEither(Future(requestParameters(request)))
    clientId <- fromEither(Future(clientId(parameters)))
    responseType <- fromEither(Future(responseType(parameters)))
    client <- fromEither(client(clientId))
    response <- fromEither[Future, ErrorResponse, Client](Future(validateResponseType(client, responseType).toLeft(client)))
  } yield response

val x: Future[\/[ErrorResponse, Client]] = result.run

Upvotes: 13

johanandren
johanandren

Reputation: 11479

There is no really clean way to do comprehensions over multiple monad types. In ScalaZ there is OptionT that might help, worth checking out. You could also transform your Eithers to Options or the other way around and be able to have a little bit less of a mess. A third option might be to create your own kind of wrapper that combines Future[Either|Option] into the same monad and then comprehend over that.

For reference I asked aboutish the same question on the play framework mailing list recently and got some good links in the replies: https://groups.google.com/d/topic/play-framework/JmCsXNDvAns/discussion

Upvotes: 0

Related Questions