在 for-comprehension 中组合多个不同的 monad 类型

Composing multiple different monad types in a for-comprehension

Previous Title: Composing DBIOs in for-comprehension

我不明白,为什么下面的代码甚至不能编译。

我想做什么/背景

对于电影门票销售条目列表中的每个条目,如果在我的数据库中找到该电影,则将其插入。

问题似乎是,我不能在 for-comprehensions 中使用 DBIO。这是为什么?是因为我在同一个地方使用了不同类型的单子来理解吗?

val movieTicketSaleNumbers: List[MovieTicketSale] = cinemaApi.allMovieTicketSales

val insertMetricActions: List[DBIO[UUID]] = for {
  movieTicketSaleNumber: MovieTicketSale <- movieTicketSaleNumbers
  isInDatabaseAction: DBIO[Option[Movie]] = moviesDb.findOneExact(movieTicketSaleNumber.movie.id)
  optionalMovie: Option[Movie] <- isInDatabaseAction
  movieInDatabase: Movie <- optionalMovie
  insertMovieNumbersInDatabaseAction: DBIO[UUID] = insertMovieTicketSale(movieTicketSaleNumber, movieInDatabase)
  movieNumberDbId: UUID <- insertMovieNumbersInDatabaseAction
} yield movieNumberDbId

编译器输出:

[error]  found   : slick.dbio.DBIOAction[java.util.UUID,slick.dbio.NoStream,slick.dbio.Effect.All]
[error]  required: Option[?]
[error]         movieNumberDbId: UUID <- insertMovieNumbersInDatabaseAction
[error]                        ^
[error] [PROJECTPATHPLACEHOLDER]: type mismatch;
[error]  found   : Option[Nothing]
[error]  required: slick.dbio.DBIOAction[?,?,?]
[error]         movieInDatabase: Movie <- optionalMovie
[error]                              ^
[error] [PROJECTPATHPLACEHOLDER]: type mismatch;
[error]  found   : slick.dbio.DBIOAction[Nothing,Nothing,slick.dbio.Effect.All with slick.dbio.Effect]
[error]  required: scala.collection.GenTraversableOnce[?]
[error]         optionalMovie: Option[Movie] <- isInDatabaseAction
[error]                                    ^
[error] three errors found
[error] (Compile / compileIncremental) Compilation failed

是的,这是因为您在 for 理解中使用了不同类型的 monad。

想想不加糖的版本。 Scala for comprehensions 归结为一系列 mapflatMap 调用。 flatMap 的类型基本上是这样定义的:

def flatMap[F[_], A, B](item: F[A])(fn: A => F[B]): F[B]

请注意,虽然内部类型发生变化,但包装类型始终是同一类型 F。在这里,您将 DBIO 效果类型与相同的 Option 混合在一起以进行理解——这违反了 flatMap 的定义.

在你的情况下,如果你想让整个事情保持在 for 理解中,你可以尝试来自 Cats 的 OptionT monad 转换器:https://typelevel.org/cats/datatypes/optiont.htmlOptionT 本质上提供了一个包装器,允许您将 monadic 值 F[Option[_]] 本身视为 monadic 值。请注意,您还有一个 List,这是第三种 monadic 类型。所以你的计算最终可能看起来像:

import cats._
import cats.data._
import cats.implicits._

val movieTicketSaleNumbers: List[MovieTicketSale] = cinemaApi.allMovieTicketSales

def insertTicket(sale: MovieTicketSale): OptionT[DBIO, UUID] = 
  for {
    movie <- OptionT(moviesDb.findOneExact(sale.movie.id))
    movieNumberDbId <- OptionT.liftF(insertMovieTicketSale(sale, movie))
  } yield movieNumberDbId

val insertMetricActions: List[DBIO[Option[UUID]]] = movieTicketSaleNumbers.map(insertTicket(_).value)

这将为您提供包含已插入的可选 UUID 的效果列表。

虽然你不需要猫来做这件事。你可以在 vanilla Scala 中做你想做的事,尽管它有点笨拙:

val movieTicketSaleNumbers: List[MovieTicketSale] = cinemaApi.allMovieTicketSales

def insertTicket(sale: MovieTicketSale): DBIO[Option[UUID]] = 
  for {
    movie <- moviesDb.findOneExact(sale.movie.id)
    movieNumberDbId <- movie.map(insertMovieTicketSale(sale, _).map(Option(_))).getOrElse(DBIO.successful(None))
  } yield movieNumberDbId


val insertMetricActions: List[DBIO[Option[UUID]]] = movieTicketSaleNumbers.map(insertTicket(_))

可能有更优雅的表达方式,尤其是将 Option[DBIO[UUID]] 转换为 DBIO[Option[UUID]]

希望对您有所帮助!