So I was reading the "Scala with Cats" book, and there was this sentence which I'm going to quote down here:
Note that Scala’s Futures aren’t a great example of pure functional programming because they aren’t referentially transparent.
And also, an example is provided as follows:
val future1 = {
// Initialize Random with a fixed seed:
val r = new Random(0L)
// nextInt has the side-effect of moving to
// the next random number in the sequence:
val x = Future(r.nextInt)
for {
a <- x
b <- x
} yield (a, b)
}
val future2 = {
val r = new Random(0L)
for {
a <- Future(r.nextInt)
b <- Future(r.nextInt)
} yield (a, b)
}
val result1 = Await.result(future1, 1.second)
// result1: (Int, Int) = (-1155484576, -1155484576)
val result2 = Await.result(future2, 1.second)
// result2: (Int, Int) = (-1155484576, -723955400)
I mean, I think it's because of the fact that r.nextInt
is never referentially transparent, right? since identity(r.nextInt)
would never be equal to identity(r.nextInt)
, does this mean that identity
is not referentially transparent either? (or Identity monad, to have better comparisons with Future). If the expression being calculated is RT, then the Future
would also be RT:
def foo(): Int = 42
val x = Future(foo())
Await.result(x, ...) == Await.result(Future(foo()), ...) // true
So as far as I can reason about the example, almost every function and Monad type should be non-RT. Or is there something special about Future
? I also read this question and its answers, yet couldn't find what I was looking for.
CodePudding user response:
You are actually right and you are touching one of the pickiest points of FP; at least in Scala.
Technically speaking, Future
on its own is RT. The important thing is that different to IO
it can't wrap non-RT things into an RT description. However, you can say the same of many other types like List
, or Option
; so why folks don't make a fuss about it?
Well, as with many things, the devil is in the details.
Contrary to List
or Option
, Future
is typically used with non-RT things; e.g. an HTTP request or a database query. Thus, the emphasis folks give in showing that Future
can't guarantee RT in those situations.
More importantly, there is only one reason to introduce Future
on a codebase, concurrency (not to be confused with parallelism); otherwise, it would be the same as Try
. Thus, controlling when and how those are executed is usually important.
Which is the reason why cats recommends the use of IO
for all use cases of Future
Note: You can find a similar discussion on this cats PR and its linked discussions: https://github.com/typelevel/cats/pull/4182
CodePudding user response:
So... the referential transparency simply means that you should be able to replace the reference with the actual thing (and vice versa) without changing the overall symatics or behaviour. Like mathematics is.
So, lets say you have x = 4
and y = 5
, then x y
, 4 y
, x 5
, and 4 5
are pretty much the same thing. And can be replaced with each otherwhenever you want.
But... just look at following two things...
val f1 = Future { println("Hi") }
val f2 = f1
val f1 = Future { println("Hi") }
val f2 = Future { println("Hi") }
You can try to run it. The behaviour of these two programs is not going to be the same.
Scala Future
are eagerly evaluated... which means that there is no way to actually write Future { println("Hi") }
in your code without executing it as a seperate behaviour.
Keep in mind that this is not just linked to having side effects
. Yes, the example which I used here with println
was a side effect
, but that was just to make the behaviour difference obvious to notice.
Even if you use something to suspend
the side effect
inside the Future
, you will endup with two suspended side effects
instead of one. And once these suspended side effects
are passed to the interpreater
, the same action will happen twice.
In following example, even if we suspend the print side-effect
by wrapping it up in an IO, the expansive evaluation part of the program can still cause different behavours even if everything in the universe is exactly same for two cases.
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
// cpu bound
// takes around 80 miliseconds
// we have only 1 core
def veryExpensiveComputation(input: Int): Int = ???
def impl1(): Unit = {
val f1 = Future {
val result = veryExpensiveComputation(10)
IO {
println(result)
result
}
}
val f2 = f1
val f3 = f1
val futures = Future.sequence(Seq(f1, f2, f3))
val ios = Await.result(futures, 100 milli)
}
def impl2(): Unit = {
val f1 = Future {
val result = veryExpensiveComputation(10)
IO {
println(result)
result
}
}
val f2 = Future {
val result = veryExpensiveComputation(10)
IO {
println(result)
result
}
}
val f3 = Future {
val result = veryExpensiveComputation(10)
IO {
println(result)
result
}
}
val futures = Future.sequence(Seq(f1, f2, f3))
val ios = Await.result(futures, 100 milli)
}
The first impl will cause only 1 expensive computation, but the second will trigger 3 expensive computations. And thus the program will fail with timeout in the second example.
If properly written with IO
or ZIO
(without Future
), it with fail with timeout in both implementations.