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)
}
}
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!
Just so you know, there's a logger in log4cats that allows you to read written messages: https://github.com/ChristopherDavenport/log4cats/blob/master/core/shared/src/main/scala/io/chrisdavenport/log4cats/extras/WriterLogger.scala
Most helpful comment
@jchapuis if you have
Syncin 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
Syncand 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
and you have one implementation using
Sync[F]to lift the current time into an instant, instead of gettingSync[F]to get that instance in tests, you could write an instance of thisTimeProvider[Id]that returns a constant value. SayTimeProvider.const(Instant.of(...)).