简体   繁体   中英

Converting a (List of Future of Either) into a (Future of Either of List) in Scala

I have a situation in a pet Scala project that I don't really know how to overcome.

The following example shows my problem.

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

case class MyBoard(id: Option[Int], name: String)
case class MyList(id: Option[Int], name: String, boardId: Option[Int] = None)
case class ErrorCreatingList(error: String)

def createList(myList: MyList): Future[Either[ErrorCreatingList, MyList]] =
  Future {
    // Let's close our eyes and pretend I'm calling a service to create this list
    Right(myList)
  }

def createLists(myLists: List[MyList],
                myBoard: MyBoard): Future[Either[ErrorCreatingList, List[MyList]]] = {

  val listsWithId: List[Future[scala.Either[ErrorCreatingList, MyList]]] =
    myLists.map { myList =>
      createList(myList.copy(boardId = myBoard.id))
    }

  //  Meh, return type doesn't match
  ???
}

I wanted createLists to return Future[Either[ErrorCreatingList, List[MyList]]] but I don't know how to do it, because listsWithId has the type List[Future[scala.Either[ErrorCreatingList, MyList]]] , which makes sense.

Is there a way to do it? A friend told me "and that's what Cats is for", but is it the only option, I mean, can't I do it using just what's in the Scala core library?

Thanks.

Use Future.sequence on your List[Future[???]] to make Future[List[???]]

val listOfFuture: List[Future[???]] = ???

val futureList: Future[List[???]] = Future.sequence(listOfFuture)

Here is how you can do it with Cats:

listFutureEither.traverse(EitherT(_)).value

Here is how one can quickly see that "there must be something like this in scala-cats already":

  • Future is a monad
  • Future[Either[E, ?]] is essentially EitherT[E, Future, ?] , therefore it's also a monad
  • Every Monad is automatically an Applicative
  • So, M[X] = EitherT[E, Future, X] is an Applicative
  • For every applicative A and traversable T , it is trivial to swap T[A[X]] into A[T[X]] .
  • List has a Traverse instance
  • you should be able to use Traverse[List] to get from List[EitherT[E, Future, X]] to EitherT[E, Future, List[X]]
  • From there, it's trivial to get to Future[Either[E, List[X]]]

Translating this step-by-step explanation into code yields:

// lines starting with `@` are ammonite imports of dependencies,
// add it to SBT if you don't use ammonite
@ import $ivy.`org.typelevel::cats-core:1.1.0`
@ import cats._, cats.data._, cats.implicits._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Either

// your input
val listFutureEither: List[Future[Either[String, Int]]] = Nil 

// monad transformer stack appropriate for the problem
type M[X] = EitherT[Future, String, X]

// converting input into monad-transformer-stack
val listM = listFutureEither.map(EitherT[Future, String, Int](_))

// solving your problem
val mList = Traverse[List].sequence[M, Int](listM)

// removing all traces of the monad-transformer-stack
val futureEitherList: Future[Either[String, List[Int]]] = mList.value

Fusing map + sequence into traverse and cleaning up some unnecessary type parameters results in the much shorter solution above.

So, val eithers = Future.traverse(myLists)(createList) will give you Future[List[Either[ErrorCreatingList, MyList]]] .

You can now transform it to what you want, but that depends on how you want to deal with the errors. What happens if some requests returned an error, and others succeeded?

This example returns Right[List[MyList]] if everything succeeded, and Left with the first error otherwise:

type Result = Either[ErrorCreatingList, List[MyList]]
val result: Future[Result] = eithers.map { 
   _.foldLeft[Result](Right(Nil)) { 
     case (Right(list), Right(myList)) => Right(myList :: list)
     case (x @ Left(_), _) => x
     case (_, Left(x)) => Left(x)
  }.right.map(_.reverse)
}

I am not cats expert, but I think the only thing it helps with here is not having to type .right before .map at the end ... but scala 2.12 does that by default too.

There is another library, called scalactic , that adds some interesting features, letting you combine multiple errors together ... but you'd have to have errors on the right for that to work ... which would be incompatible with pretty much everything else. It's not hard to combine those errors "manually" if you have to, I'd say just do that rather than switching to scalactic, which, besides being incompatible, has a considerable learning curve and hurts readability.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM