Search code examples
scalafunctional-programmingmonadsscala-catscats-effect

Complex monad transformer for IO monad


I am trying to write a Cats MTL version of a function that would save an entity to a database. I want this function to read some SaveOperation[F[_]] from environment, execute it and handle possible failure. So far I came up with 2 version of this function: save is the more polymorphic MTL version and save2 uses exact monads in its signature, meaning that I confine myself to use of IO.

  type SaveOperation[F[_]] = Employee => F[Int]

  def save[F[_] : Monad](employee: Employee)(implicit
                                     A: Ask[F, SaveOperation[F]],
                                     R: Raise[F, AppError]): F[Unit] =
    for {
      s <- A.ask
      rows <- s(employee)
      res <- if rows != 1 then R.raise(FailedInsertion)
             else ().pure[F]
    } yield res

  def save2(employee: Employee): Kleisli[IO, SaveOperation[IO], Either[AppError, Unit]] =
    Kleisli((saveOperation) => saveOperation(employee)
      .handleErrorWith(err => IO.pure(Left(PersistenceError(err))))
      .map(rows =>
        if rows != 1 then Left(FailedInsertion)
        else Right(())
      )
    )

I can later call those like this:

  val repo = new DoobieEmployeeRepository(xa)
  val employee = Employee("john", "doe", Set())
  type E[A] = Kleisli[IO, SaveOperation[IO], Either[AppError, A]]
  println(EmployeeService.save[E](employee).run(repo.save).unsafeRunSync())
  println(EmployeeService.save2(employee).run(repo.save).unsafeRunSync())

The problem is that for the call of save I get the following error:

Could not find an instance of Monad for E.
I found:

    cats.data.Kleisli.catsDataMonadErrorForKleisli[F, A, E]

But method catsDataMonadErrorForKleisli in class KleisliInstances0_5 does not match type cats.Monad[E].

This error doesn't seem to make sense to me as effectively signatures are exactly the same for both function, so the monad should be there. I suspect the problem is with Ask[F, SaveOperation[F]] parameter as here F is not IO, while SaveOperation needs the IO.

Why can't I use the Kleisli monad for save call?

Update:

If I modify the type to type E[A] = EitherT[[X] =>> Kleisli[IO, SaveOperation[IO], X], AppError, A], I get a new error:

Could not find an implicit instance of Ask[E, SaveOperation[E]] 

The right generic type for SaveOperation is supposed to be IO I guess, but I can't figure how to properly provide it through an instance of Ask


Solution

  • I hope you don't mind if I use this opportunity to do a quick tutorial on how to improve your question. It not only increases the chances of someone answering, but also might help you to find the solution yourself.

    There are a couple of problems with the code you submitted, and I mean problems in terms of being a question on SO. Perhaps someone might have a ready answer just by looking at it, but let's say they don't, and they want to try it out in a worksheet. Turns out, your code has a lot of unnecessary stuff and doesn't compile.

    Here are some steps you could take to make it better:

    • Strip away the unnecessary custom dependencies like Employee, DoobieEmployeeRepository, error types etc. and replace them with vanilla Scala types like String or Throwable.
    • Strip away any remaining code as long as you can still reproduce the problem. For example, the implementations of save and save2 are not needed, and neither are Ask and Raise.
    • Make sure that the code compiles. This includes adding the necessary imports.

    By following these guidelines, we arrive at something like this:

    import cats._
    import cats.data.Kleisli
    import cats.effect.IO
    
    type SaveOperation[F[_]] = String => F[Int]
    
    def save[F[_] : Monad](s: String)(): F[Unit] = ???
    def save2(s: String): Kleisli[IO, SaveOperation[IO], Either[Throwable, Unit]] = ???
    
    type E[A] = Kleisli[IO, SaveOperation[IO], Either[Throwable, A]]
    
    println(save[E]("Foo")) // problem!
    println(save2("Bar"))
    

    That's already much better, because a) it allows people to quickly try out your code, and b) less code means less cognitive load and less space for problems.

    Now, to check what's happening here, let's go to some docs: https://typelevel.org/cats/datatypes/kleisli.html#type-class-instances

    It has a Monad instance as long as the chosen F[_] does.

    That's interesting, so let's try to further reduce our code:

    type E[A] = Kleisli[IO, String, Either[Throwable, A]] 
    implicitly[Monad[E]] // Monad[E] doesn't exist
    

    OK, but what about:

    type E[A] = Kleisli[IO, String, A] 
    implicitly[Monad[E]] // Monad[E] exists!
    

    This is the key finding. And the reason that Monad[E] doesn't exist in the first case is:

    Monad[F[_]] expects a type constructor; F[_] is short for A => F[A] (note that this is actually Kleisli[F, A, A] :)). But if we try to "fix" the value type in Kleisli to Either[Throwable, A] or Option[A] or anything like that, then Monad instance doesn't exist any more. The contract was that we would provide the Monad typeclass with some type A => F[A], but now we're actually providing A => F[Either[Throwable, A]]. Monads don't compose so easily, which is why we have monad transformers.

    EDIT:

    After a bit of clarification, I think I know what you're going after now. Please check this code:

      case class Employee(s: String, s2: String)
      case class AppError(msg: String)
    
      type SaveOperation[F[_]] = Employee => F[Int]
    
      def save[F[_] : Monad](employee: Employee)(implicit
                                                 A: Ask[F, SaveOperation[F]],
                                                 R: Raise[F, AppError]): F[Unit] = for {
          s <- A.ask
          rows <- s(employee)
          res <- if (rows != 1) R.raise(AppError("boom"))
          else ().pure[F]
        } yield res
    
      implicit val askSaveOp = new Ask[IO, SaveOperation[IO]] {
    
        override def applicative: Applicative[IO] =
          implicitly[Applicative[IO]]
    
        override def ask[E2 >: SaveOperation[IO]]: IO[E2] = {
          val fun = (e: Employee) => IO({println(s"Saved $e!"); 1})
          IO(fun)
        }
      }
    
      implicit val raiseAppErr = new Raise[IO, AppError] {
    
        override def functor: Functor[IO] = 
          implicitly[Functor[IO]]
    
        override def raise[E2 <: AppError, A](e: E2): IO[A] = 
          IO.raiseError(new Throwable(e.msg))
      }
    
      save[IO](Employee("john", "doe")).unsafeRunSync() // Saved Employee(john,doe)!
    

    I'm not sure why you expected Ask and Raise to already exist, they are referring to custom types Employee and AppError. Perhaps I'm missing something. So what I did here was that I implemented them myself, and I also got rid of your convoluted type E[A] because what you really want as F[_] is simply IO. There is not much point of having a Raise if you also want to have an Either. I think it makes sense to just have the code based around F monad, with Ask and Raise instances that can store the employee and raise error (in my example, error is raised if you return something other than 1 in the implementation of Ask).

    Can you check if this is what you're trying to achieve? We're getting close. Perhaps you wanted to have a generic Ask defined for any kind of SaveOperation input, not just Employee? For what it's worth, I've worked with codebases like this and they can quickly blow up into code that's hard to read and maintain. MTL is fine, but I wouldn't want to go more generic than this. I might even prefer to pass the save function as a parameter rather than via Ask instance, but that's a personal preference.