在一个备受宠爱的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核心库中的内容吗?
谢谢。
在List[Future[???]]
上使用Future.sequence
来制作Future[List[???]]
val listOfFuture: List[Future[???]] = ???
val futureList: Future[List[???]] = Future.sequence(listOfFuture)
以下是如何使用Cats:
listFutureEither.traverse(EitherT(_)).value
以下是如何快速发现"scala猫中一定已经有这样的东西了":
Future
是monadFuture[Either[E, ?]]
本质上是EitherT[E, Future, ?]
,因此它也是monad- 每个
Monad
自动成为Applicative
- 所以,
M[X] = EitherT[E, Future, X]
就是Applicative
- 对于每个可应用的
A
和可遍历的T
,将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
中,并清除一些不必要的类型参数,可以得到更短的解决方案。
因此,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)
}
我不是cats
专家,但我认为它在这里唯一有帮助的是不必在最后的.map
之前键入.right
。。。但scala2.12默认情况下也会这样做。
还有另一个库,名为scalactic
,它添加了一些有趣的功能,可以将多个错误组合在一起。。。但你必须在右边有错误才能奏效。。。这将与几乎所有其他内容都不兼容。如果必须的话,"手动"组合这些错误并不难,我想说,只是这样做,而不是切换到标量,标量除了不兼容之外,还有相当大的学习曲线,并损害可读性。