I'm looking at cats.effect.concurrent.Deferred and noticed that all pure factory methods inside its companion object return the F[Deferred[F, A]], not just Deferred[F, A] like
def apply[F[_], A](implicit F: Concurrent[F]): F[Deferred[F, A]] =
F.delay(unsafe[F, A])
but
/**
* Like `apply` but returns the newly allocated promise directly instead of wrapping it in `F.delay`.
* This method is considered unsafe because it is not referentially transparent -- it allocates
* mutable state.
*/
def unsafe[F[_]: Concurrent, A]: Deferred[F, A]
Why?
The abstract class has two methods defined (docs omitted):
abstract class Deferred[F[_], A] {
def get: F[A]
def complete(a: A): F[Unit]
}
So even if we allocate Deferred directly it is not clear how the state of Deferred can be modified via its public method. All modification are suspended with F[_].
The issue isn't whether or not mutation is suspended in
F, it's whetherDeferred.unsafeallows you to write code that isn't referentially transparent. Consider the following two programs:These two programs aren't equivalent:
p1will compute1andp2will wait forever. The fact that we can construct an example like this shows thatDeferred.unsafeisn't referentially transparent—we can't freely replace calls to it with references and end up with equivalent programs.If we try to do the same thing with
Deferred.apply, we'll find that we can't come up with pairs of non-equivalent programs by replacing references with values. We could try this:…but that gives us two programs that are equivalent (both hang). Even if we try something like this:
…all referential transparency tells us is that we can rewrite that code to the following:
…which is equivalent to
p3, so we've failed to break referential transparency again.The fact that we can't get a reference to the mutable
Deferred[IO, Int]outside of the context ofFwhen we useDeferred.applyis specifically what's protecting us here.