Previous Title: Composing DBIOs in for-comprehension
I don't understand, why the following code does not even compile.
What I Want To Do / Context
For each entry in a list of ticket sale entries for movies, insert it, if the movie is found in my database.
The problem seems to be, that I cant use DBIO in for-comprehensions. Why is that? Is it because i'm using different types of monads in the same for comprehension?
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
Compiler Output:
[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
Yes, it's because you're using different types of monads in the for comprehension.
Think about the unsugared version. Scala for comprehensions boil down to a series of map
and flatMap
calls. The type of flatMap
is defined essentially like this:
def flatMap[F[_], A, B](item: F[A])(fn: A => F[B]): F[B]
Note that while the inner type changes, the wrapping type is always of the same type F. Here, you're mixing a DBIO effect type with an Option in the same for comprehension--that violates the definition of flatMap.
In your case, if you want to keep the whole thing in a for
comprehension, you can try the OptionT
monad transformer from Cats: https://typelevel.org/cats/datatypes/optiont.html. OptionT
essentially provides a wrapper that allows you to treat the monadic value F[Option[_]]
as a monadic value in itself. Note that you've got a List as well, which is a third monadic type. So your computation might end up looking like:
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)
That will give you a list of effects wrapping optional UUIDs that were inserted.
You don't need Cats to do this, though. You can do what you want in vanilla Scala, though it's a quite a bit clunkier:
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(_))
There's probably a more elegant way to express this, especially the conversion of the Option[DBIO[UUID]]
to the DBIO[Option[UUID]]
.
Hope that helps!