4

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.

AminMal
  • 3,070
  • 2
  • 6
  • 15
  • In my opinion, Luka's answer you linked is perfect. I'd be happy to try and help you further, but you'd have to specify why you don't feel that his answer has hit the nail on the head. Because it did :) with IO monads, you get the referential transparency that `Future` (due to its eager initialisation) cannot provide. And the `r.nextInt` problem you described, that's due to the non-referential transparency of the randomizer, and has nothing to do with the `Future` itself; the same example would hold if you replaced the `Future` with `IO` or anything else (a `List`, `Option`, `Try` ...). – slouc Oct 15 '22 at 20:12
  • Have a look at the last paragraph in [this answer on JavaScript promises](https://stackoverflow.com/a/45772042/1048572) for a better example – Bergi Oct 15 '22 at 20:57
  • @slouc What I don't get is, the eager instantiation causes `Future` to not to be an effect, right? since execution is not separated from construction. But this has nothing to do with begin `RT` (as far as I could reason), its more related to effects. If you take a look at Luis Suarez's answer, the same thing happens for Seq and many other things, why people don't say that Seq isn't RT or things like that? And also, RT aspect of `Future` is directly related to the expression being evaluated (which makes Future not RT *all the time*, same thing for other common Scala Monads). – AminMal Oct 16 '22 at 12:01
  • But the thing is, in your example, it's not the fault of Future/Seq/other monads that you can put some non-referentially-transparent stuff inside (it is Scala's fault). Your randomizer example has internal state, and that makes it non-RT, regardless of what kind of other structure you place it in. If you want to solve the problem of state in RT way, you should use a State monad. Try working through this, it actually uses the randomizer as example: https://typelevel.org/cats/datatypes/state.html – slouc Oct 16 '22 at 13:51

2 Answers2

6

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

  • Thanks for the answer, this is actually what I was looking for. I also have another question that might be a bit off-topic to your answer, but it would be great if you could help. As you might be able to guess, I'm currently trying to learn Category theory and effect system, but I couldn't find any good resource for effect systems. Do you know any good resource that might help? – AminMal Oct 16 '22 at 12:05
  • 1
    @AminMal personal advice, don't focus on category theory, instead focus on learning the _"Programs as Values"_ paradigm which is the main idea behind effect systems. I have some examples and resources here: https://github.com/pslcorp/programs-as-values – Luis Miguel Mejía Suárez Oct 16 '22 at 14:26
1

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.

sarveshseri
  • 13,738
  • 28
  • 47
  • It is actually linked to having _side-effects_. `Future` is not less RT than `Option`, the important detail is on the intention. – Luis Miguel Mejía Suárez Oct 15 '22 at 19:40
  • What if I use an `IO` to suspend the side effect inside the future. I will still be left with two different `Future[IO[Unit]]`. – sarveshseri Oct 15 '22 at 19:44
  • Not sure what is your point, if you create a `val f = Future(IO.println("Foo"))` and then you do `val x = (f, f)` it behaves the same as if you do `val x = (Future(IO.println("Foo")), Future(IO.println("Foo")))` which is the classical example of breaking RT. Even if you compose the `Future` to run the `IO` you will get always the same output, thus showing it is RT. – Luis Miguel Mejía Suárez Oct 15 '22 at 19:45
  • Not it does not. The program using `val x = (f, f)` will print `Foo` only once but the second one will print `Foo` twice. – sarveshseri Oct 15 '22 at 19:48
  • 1
    No, neither of them will print anything at all. – Luis Miguel Mejía Suárez Oct 15 '22 at 19:51
  • I am talking about the program using these two. Not these snippets themself. – sarveshseri Oct 15 '22 at 19:53
  • Please, create such a program. As I said, you will either see that both programs will have the same output, thus preserving RT or you would be forced to use something like `unsafeRunSync()` inside one of the `Futures` in order to get different output. Thus, showing again that to break RT using `Futures` you have to use a non-RT expression in the first place; which implies that fi you replace `Future` with `Option` or any other `Monad` you would get the same behaviour, implying there is nothing wrong with `Future` _per se_. – Luis Miguel Mejía Suárez Oct 15 '22 at 19:56
  • @LuisMiguelMejíaSuárez It seems that way because you are only focusing on the `side-effect` part. What about pure computation... does that not impact the program behaviour (given same state of universe for both cases)? – sarveshseri Oct 15 '22 at 22:44
  • 2
    Have you read my comments on the **cats** PR that I shared? I personally think `Future` is not great even for pure computations, because you can't control execution order and concurrency _(which is the only motivation for using it)_. However, from a pure RT perspective, `Future` is isomorphic to `Try` – Luis Miguel Mejía Suárez Oct 15 '22 at 23:04
  • While using time is a great example of why I think `Future` is not a good programming abstraction. It really doesn't do much about RT, since it is pretty easy to cause CPU differences using `IO` as well. It would also invalidate many rewrite rules that derive from RT; and most importantly RT doesn't says anything about execution time – Luis Miguel Mejía Suárez Oct 16 '22 at 00:17