Search code examples
scalascala-cats

How to use the MonadError with the effect type IO?


I am trying to write tests for my tagless algebra that it use the MonadError.

Here is tagless algebra with the interpreter:

trait DbConnector[F[_]] {
  def read(url: DbUrl, user: DbUser, pw: DbPw): F[DbParams]
}


object DbConnector {

  def apply[F[_]](implicit dc: DbConnector[F]): dc.type = dc

  def impl[F[_] : MonadError[*[_], Throwable]](env: Environment[F])
  : DbConnector[F] =
    new LiveDbConnector[F](env)

}

and the test class with MonadError instance of Either:

class DbConnectorSpec extends munit.FunSuite {
  test("Environment variables are not set") {

    val monadError = MonadError[IO[Either[DbError, DbParams]], DbError]
    implicit val badEnv = DbConnector.impl(LiveBadEnvironment())


  }
}

the code does not get compiled, because of the error message:

cats.effect.IO[Either[io.databaker.db.DbError,io.databaker.db.DbParams]] takes no type parameters, expected: 1
[error]     val monadErr = MonadError[IO[Either[DbError, DbParams]], DbError]

The MonadError is new to me and that is the first time, I am trying to use it. I have read the concept of the MonadError on https://www.scalawithcats.com/dist/scala-with-cats.html.

How to make it run?


Solution

  • Consider the type parameter clause of MonadError

    trait MonadError[F[_], E]
    

    Note how F and E are of different shapes (or kinds):

    F - a type constructor of * -> * kind
    E - a proper type of * kind
    

    The difference between a type constructor and a proper type is like the difference between List and List[Int], that is, a type constructor List needs a type argument Int to construct a proper type List[Int].

    Now consider the kind of IO[Either[Throwable,Int]]

    scala> :kind -v IO[Either[Throwable, Int]]
    cats.effect.IO[Either[Throwable,Int]]'s kind is A
    *
    This is a proper type.
    

    We see it has the shape of the proper type so it will not fit in place of F

    scala> MonadError[IO[Either[Throwable, Int]], Throwable]
    <console>:25: error: cats.effect.IO[Either[Throwable,Int]] takes no type parameters, expected: one
           MonadError[IO[Either[Throwable, Int]], Throwable]
    

    Now consider the kind of IO

    scala> :kind -v IO
    cats.effect.IO's kind is F[+A]
    * -(+)-> *
    This is a type constructor: a 1st-order-kinded type.
    

    We see it is a type constructor of * -> * shape which matches the shape of F type constructor. Therefore we can write

    scala> MonadError[IO, Throwable]
    res2: cats.MonadError[cats.effect.IO,Throwable] = cats.effect.IOLowPriorityInstances$IOEffect@75c81e89
    

    Here are some further examples

    import cats._
    import cats.data._
    import cats.effect.IO
    import cats.instances.either._
    import cats.instances.try_._
    import cats.instances.future._
    import scala.concurrent.Future
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.util.Try
    
    MonadError[Future, Throwable]
    MonadError[Try, Throwable]
    MonadError[IO, Throwable]
    MonadError[Either[String, *], String]
    MonadError[EitherT[IO, String, *], String]
    MonadError[EitherT[Future, String, *], String]
    

    Note the * in Either[String, *] syntax comes from kind-projector and is an alternative to using a type alias to convert, for example, * -> * -> * kind into required * -> * kind

    scala> :kind -v Either[String, *]
    scala.util.Either[String,?]'s kind is F[+A]
    * -(+)-> *
    This is a type constructor: a 1st-order-kinded type.
    
    scala> type MyError[+A] = Either[String, A]
    defined type alias MyError
    
    scala> :kind -v MyError
    MyError's kind is F[+A]
    * -(+)-> *
    This is a type constructor: a 1st-order-kinded type.