在Scala中将“(List of Future of Either)”转换为“(Future of Either of List)”

4

我在一个Scala宠物项目中遇到了一些问题,我不知道如何解决。

下面的例子展示了我的问题。

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

我希望createLists返回Future [Either [ErrorCreatingList,List [MyList]]],但我不知道如何做到这一点,因为listsWithId的类型为List [Future [scala.Either [ErrorCreatingList,MyList]]],这是有道理的。
有没有办法做到这一点? 一个朋友告诉我“这就是Cats的用途”,但它是唯一的选择吗? 我的意思是,我不能只使用Scala核心库来完成吗?
谢谢。

2
建议阅读:https://blog.buildo.io/monad-transformers-for-the-working-programmer-aa7e981190e7 - 它提出了一个相似的问题和一个相对简单的解决方案,这个解决方案有一个“大名”(“Monad Transformer”),但它是一个简单、易于理解的概念,可能也会在这里有所帮助。 - Tzach Zohar
可能是将多个Future[Seq]连接成一个Future[Seq]的重复问题。 - Suma
3个回答

4

在你的 List[Future[???]] 上使用 Future.sequence 来创建 Future[List[???]]

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

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

5
这将创建一个“要么”的未来列表...这不是提问者想要的。 - Dima
1
这实际上是我的错,因为问题的标题有误导性。 - user978080

4
以下是如何使用Cats进行操作的方法:
listFutureEither.traverse(EitherT(_)).value

以下是如何快速了解“Scala-Cats中一定有类似的东西”的方法:
- Future 是一个单子(monad) - Future[Either[E,?]] 本质上是 EitherT[E,Future,?],因此它也是一个单子 - 每个 Monad 自动成为一个 Applicative - 因此,M[X] = EitherT[E,Future,X] 是一个 Applicative - 对于每个可遍历的 Traversable[T] 和 Applicative[A],将 T[A[X]] 转换为 A[T[X]] 是微不足道的 - List 有一个 Traverse 实例 - 应该可以使用 Traverse[List] 将 List[EitherT[E,Future,X]] 转换为 EitherT[E,Future,List[X]] - 从那里,将其轻松转换为 Future[Either[E,List[X]]]
将这个逐步解释转化成代码如下:
// 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

map + sequence融合为traverse并清理一些不必要的类型参数,可以得到上面更简洁的解决方案。


问题在于,在这种情况下,遍历不会停止在第一个左侧结果上,它将执行所有的 Futures。 - Sergey Belash
1
@SergeyBelash 很抱歉,这不是 Traverse 的问题,而是因为一般情况下 Future 无法在不立即开始计算它们正在计算的内容的副作用的情况下创建。因此,如果您有一个包含 n 个某些东西的 Future 列表,则将计算 n 个某些东西,而不管您稍后如何处理生成的列表。如果这不是您想要的行为,则应该使用您喜欢的某种 Task 实现替换 Scala 的默认 Future,以便在您要求之前不会启动任何计算。 - Andrey Tyukin
是的!感谢您的解释。实际上,我尝试使用了monix.Task,结果也是一样的。但我猜这是因为我尝试使用“.runAsync”,它只是将任务转换为Futures。 - Sergey Belash

2

因此,val eithers = Future.traverse(myLists)(createList) 将给你一个 Future[List[Either[ErrorCreatingList, MyList]]]

现在您可以根据您处理错误的方式来进行转换。如果某些请求返回错误,而另一些请求成功了会发生什么?

这个例子将在所有操作成功时返回 Right[List[MyList]],否则会返回第一个错误的 Left

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

我不是猫的专家,但我认为它在这里唯一有用的事情就是不必在结尾处输入.right,但是scala 2.12默认也会这样做。

还有另一个名为scalactic的库,它增加了一些有趣的功能,可以让您将多个错误组合在一起...但是,您必须在右侧具有错误才能使其正常工作...这与几乎所有其他内容都不兼容。如果必须,手动合并这些错误并不难,我建议您这样做,而不是切换到scalactic,因为它除了不兼容之外,还具有相当大的学习曲线和降低可读性的缺点。


谢谢@Dima。我将尝试使用您的答案。现在,我有一些编译错误。即使在Result的定义中删除了Either周围的[]并将mylist更改为myList,我仍然在第1个和第3个case中得到“类型不匹配”的错误。我会尝试自己解决它们,并给您一些反馈,以便如果需要修复,我们可以为将来的参考进行修复。 - user978080
@Dima 不确定是否需要使用.right.,建议再次通过编译器运行。 - Andrey Tyukin
1
@AndreyTyukin 在 Scala 2.11 中需要使用 right,但在 Scala 2.12 中不需要,因为 Either 是右偏的,所以可以直接映射。 - Dima

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接