3

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

Leonid Bor
  • 2,064
  • 6
  • 27
  • 47

1 Answers1

4

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.

slouc
  • 9,508
  • 3
  • 16
  • 41
  • Thank you for your response! It helps to understand the first part of the problem, however, implicit instances and the method implementations are important here as the goal is to write correct MTL version. If I modify the type to `type E[A] = EitherT[[X] =>> Kleisli[IO, SaveOperation[IO], X], AppError, A]`, I get a new error which I believe is the one I was originally trying to refer to: `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 then as you said it will be hard to compose – Leonid Bor Oct 07 '21 at 17:11
  • 1
    OK after a bit of fiddling around I think we are getting close to being on the same page here. Please check my edit. – slouc Oct 13 '21 at 11:48
  • thanks a lot for the response, it achieves the result I need when I call `save[IO]`. My original plan was to call `save[Reader[SaveOperation[IO], Either[AppError, *]]` rather, and I struggled to provide all the implicits for such construction. Do you have any thoughts on how use of `IO` compares to use of `Reader[SaveOperation[IO], Either[AppError, *]]`? The latter seems more descriptive as it indicates that there is a dependency that performs a side effect and the result might fail, while plain `IO` doesn't tell much except presence of side effects – Leonid Bor Oct 14 '21 at 17:38
  • also, the `=>>` syntax I used is Scala 3 syntax for lambda types, this is also achieved with `*` and kind projector plugin in Scala 2 – Leonid Bor Oct 14 '21 at 17:38
  • Interesting, I haven't used Scala 3 yet. I will assume that the rest of the confusing syntax was due to that as well :) I think that your intention makes sense, but in that case I would go for `IO[Either[AppError, *]]`. Unfortunately, cats effect decided to keep `IO` free of error channel, unlike zio. But what confuses me is the reader - why do you need it? Isn't the presence of `Ask` enough to tell that there's a dependency? – slouc Oct 14 '21 at 20:51
  • yeah, I think you're right about `Reader`. I'll take a look at ZIO, meanwhile I think my question is fully answered now, thank you! – Leonid Bor Oct 14 '21 at 21:28
  • Sure! Bear in mind that `IO` doesn't have an explicit error channel, but that doesn't mean there isn't one. You can simply `IO.raiseError(MyErrorWhichExtendsThrowable(...))` whenever something goes wrong, and in the error handling part you then simply pattern match on the error type to differentiate between e.g. `AppError` and `ConnectionError`. But if you want to have your errors and your dependencies encoded in the type signature at all times, zio is definitely something you should try out. – slouc Oct 15 '21 at 06:30