Cats-effect: Instances for Id

Created on 6 Dec 2019  路  6Comments  路  Source: typelevel/cats-effect

I'm new to cats-effect, and I have a codebase with unit coverage running on Id. Even though it's trivial to write, I was surprised not to find an instance of Sync built-in for instance, so I wrote the one you can find below. Is there a specific reason why such a utility instance isn't included? Or did I miss it somehow?

Thanks a lot!

new Sync[Id] {
      def suspend[A](thunk: => Id[A]): Id[A] = thunk

      def bracketCase[A, B](acquire: Id[A])(use: A => Id[B])(release: (A, ExitCase[Throwable]) => Id[Unit]): Id[B] = {
        val a   = acquire
        val etb = attempt(use(a))
        release(a, etb match {
          case Left(e)  => ExitCase.error[Throwable](e)
          case Right(_) => ExitCase.complete
        })
        rethrow(pure(etb))
      }

      def raiseError[A](e: Throwable): Id[A] = throw e

      def handleErrorWith[A](fa: Id[A])(f: Throwable => Id[A]): Id[A] = Try(fa) match {
        case Failure(exception) => f(exception)
        case Success(value)     => value
      }

      def pure[A](x: A): Id[A] = x

      def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] = f(fa)

      @tailrec
      def tailRecM[A, B](a: A)(f: A => Id[Either[A, B]]): Id[B] = f(a) match {
        case Right(b) => pure(b)
        case Left(a)  => tailRecM[A, B](a)(f)
      }
}

Most helpful comment

@jchapuis if you have Sync in your functions but they don't directly lift side effects, you might have a design problem in your application. If they directly lift side effects, they should be tested with SyncIO or IO or equivalent.

Ideally Sync and above should only appear in the instance definitions for bottom-level algebras (implementations of the things everything else depends on).

For example, if you have

trait TimeProvider[F[_]] {
  def now: F[Instant]
}

and you have one implementation using Sync[F] to lift the current time into an instant, instead of getting Sync[F] to get that instance in tests, you could write an instance of this TimeProvider[Id] that returns a constant value. Say TimeProvider.const(Instant.of(...)).

All 6 comments

Writing Sync[Id] is impossible, because Id cannot suspend side-effects, which it's the whole purpose of Sync.
Sync is not only made by its methods, but also by its laws, and your instance wouldn't pass them. If you want, I can give you some concrete examples as to why that instance does not work, but the main key point is that it breaks referential transparency

As an initial example

  def foo[F[_]: Sync] = {
    val a: F[Unit] = Sync[F].delay(throw new Exception)

    a.handleError(_ => ()).as("success")
  }
> foo[IO].unsafeRunSync
res1: String = success

>  foo[Id]
java.lang.Exception
  at Playground$.$anonfun$foo$1(Playground.scala:61)
  at cats.effect.Sync.$anonfun$delay$1(Sync.scala:54)

Not only are exceptions a problem, but simple evaluation is as well! As an example:

def bar[F[_]: Sync] = {
  val a = Sync[F].delay(print("a"))
  val b = Sync[F].delay(print("b"))
  b >> a
}

bar[IO]   // prints "ba"
bar[Id]   // prints "ab"

You could do a bit better on both of these by using Eval rather than just Id, but at that point you may as well just use SyncIO. :-)

@jchapuis if you have Sync in your functions but they don't directly lift side effects, you might have a design problem in your application. If they directly lift side effects, they should be tested with SyncIO or IO or equivalent.

Ideally Sync and above should only appear in the instance definitions for bottom-level algebras (implementations of the things everything else depends on).

For example, if you have

trait TimeProvider[F[_]] {
  def now: F[Instant]
}

and you have one implementation using Sync[F] to lift the current time into an instant, instead of getting Sync[F] to get that instance in tests, you could write an instance of this TimeProvider[Id] that returns a constant value. Say TimeProvider.const(Instant.of(...)).

Thanks everyone for your outstanding level of support and very informative answers!

Everything makes complete sense, I should have spent more time reading up before diving in, I missed that there was more to Sync[F] than just synchronous execution.

Your answers were useful, I have a clearer understanding now. Turns out my entry door to cats-effect is actually the log4cats lib: I'm trying to put together a pure domain and I started with logging. From what I understand, the slf4j logging machinery is lifted into Logger[F] via Sync[F].

Ideally, at this stage I'd like to run tests synchronously and verify that logging entries were added for certain program flows. As you said @kubukoz and if I understand well, if I could somehow provide Logger[Id] directly in my tests that would be ideal. I will explore further along this path, thanks again to all!

Was this page helpful?
0 / 5 - 0 ratings