Search code examples
scalaslick

Composing multiple different monad types in a for-comprehension


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

Solution

  • 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!