11

I am reading the book FPiS and on the page 107 the author says:

We should note that Future doesn’t have a purely functional interface. This is part of the reason why we don’t want users of our library to deal with Future directly. But importantly, even though methods on Future rely on side effects, our entire Par API remains pure. It’s only after the user calls run and the implementation receives an ExecutorService that we expose the Future machinery. Our users therefore program to a pure interface whose implementation nevertheless relies on effects at the end of the day. But since our API remains pure, these effects aren’t side effects.

Why Future has not purely functional interface?

softshipper
  • 32,463
  • 51
  • 192
  • 400
  • 3
    I would really suspect that this question related to the debate "is Future a monad Or not" : https://stackoverflow.com/questions/27454798/is-future-in-scala-a-monad – Pavel May 26 '17 at 07:44

4 Answers4

29

The problem is that creating a Future that induces a side-effect is in itself also a side-effect, due to Future's eager nature.

This breaks referential transparency. I.e. if you create a Future that only prints to the console, the future will be run immediately and run the side-effect without you asking it to.

An example:

for {
  x <- Future { println("Foo") }
  y <- Future { println("Foo") }
} yield ()

This results in "Foo" being printed twice. Now if Future was referentially transparent we should be able to get the same result in the non-inlined version below:

val printFuture = Future { println("Foo") }

for {
  x <- printFuture
  y <- printFuture
} yield ()

However, this instead prints "Foo" only once and even more problematic, it prints it no matter if you include the for-expression or not.

With referentially transparent expression we should be able to inline any expression without changing the semantics of the program, Future can not guarantee this, therefore it breaks referential transparency and is inherently effectful.

Luka Jacobowitz
  • 22,795
  • 5
  • 39
  • 57
  • 2
    I don't see how this is specific to the future. Isn't it true about any expression that executes a function? If you give it a lambda with a side effect, it'll have a side effect ... (Duh!) :) Like ... I dunno ... `Seq(1,2,3).filter { println _; true }` Does that make `Seq` not purely functional too? – Dima May 26 '17 at 13:33
  • That is true, indeed. But most often that's how `Future` is used. If you use `Future` with a pure computation, then it should be considered referentially transparent. However, most people do not use it that way. If you compare it to the Monix task or the FS2 Task, it guarantees referential transparency for values that can capture side effects. I.e. if you try to do the same with `Task` both code snippets will behave the same way :) – Luka Jacobowitz May 26 '17 at 13:42
  • Ok ... can you explain what exactly is better about `Task` then? I mean, you can inline `val task = Task delay { prinltn("foo") }`, ok, but `task.run` is still not referentially transparent. Isn't this simply pushing the problem one step down? – Dima May 26 '17 at 13:49
  • 2
    Yup, it is indeed! Functional Programming is all about pushing the side effects to the edge of the program! :) Have a look at this article: http://typelevel.org/blog/2017/05/02/io-monad-for-cats.html – Luka Jacobowitz May 26 '17 at 14:15
  • This is a poor reason to say Future is side effecting, since if that where true, you'd need to say ALL functions in scala is side effects. Just because you can call side effecting functions inside it, it does not mean that it itself side effecting. – Máté Magyar Jun 13 '17 at 07:42
  • Well it's about referential Transparency. `Task` captures side-effects in a referential transparent way, while `Future` does not. :) – Luka Jacobowitz Jun 13 '17 at 07:43
  • This is because you made the `printFuture` as a `Value` It's evaluated even before getting into for-comprehension. You should make `printFuture` as a function instead to get the same effect. i.e) def printFuture = Future { println("Foo") } – gyoho Nov 02 '18 at 18:30
  • yes, but Future type still remains not referentially transparent. Defining the call in a function is just a hack to workaround the problem. – Fabszn Oct 26 '19 at 06:10
10

A basic premise of FP is referential transparency. In other words, avoiding side effects.

What's a side effect? From Wikipedia:

In computer science, a function or expression is said to have a side effect if it modifies some state outside its scope or has an observable interaction with its calling functions or the outside world. (Except, by convention, returning a value: returning a value has an effect on the calling function, but this is usually not considered as a side effect.)

And what is a Scala future? From the documentation page:

A Future is a placeholder object for a value that may not yet exist.

So a future can transition from a not-yet-existing-value to an existing-value without any interaction from or with the rest of the program, and, as you quoted: "methods on Future rely on side effects."

It would appear that Scala futures do not maintain referential transparency.

jwvh
  • 50,871
  • 7
  • 38
  • 64
5

As far as I know, Future runs its computation automatically when it's created. Even if it lacks side-effects in its nested computation, it still breaks flatMap composition rule, because it changes state over time:

someFuture.flatMap(Future(_)) == someFuture // can be false

Equality implementation questions aside, we can have a race condition here: new Future immediately runs for a tiny fraction of time, and its isCompleted can differ from someFuture if it is already done.

In order to be pure w.r.t. effect it represents, Future should defer its computation and run it only when explicitly asked for it, like in the case of Par (or scalaz's Task).

P. Frolov
  • 876
  • 6
  • 15
  • This is the correct answer. Although, `Task` is still side effecting as far as i know, just at an explicitly asked point. I'm not sure if you can practically do this without side-effecting. Of Course you can chanin computation, but if you are not side effecting, you need to block at some point, which is rather undesirable. – Máté Magyar Jun 13 '17 at 07:51
  • 1
    Not that it's a big problem, however - the intended place to run your `Task` in a FP program is in its entry point, and that should be the last thing you actually do there. That's exactly how all the parts of the program can stay composable, including I/O operations - they're assembled into general execution plan to be run at the very end. – P. Frolov Jun 13 '17 at 08:26
  • Fair point! Is there any good library that implements something like task, without the baggage of scalaz? – Máté Magyar Jun 13 '17 at 09:10
  • I don't really know what exactly the 'baggage' of `scalaz` everyone talking about is (mathematical naming is pretty standard and not Haskell-specific), but there's relatively new Monix library that seems to do the same thing: https://monix.io/docs/2x/eval/task.html – P. Frolov Jun 13 '17 at 10:31
  • Thanks! I don't mean it as baggage regarding the quality of it, but it's size and scope is huge, i would not want to include it in my relatively small library (right now it does not have any runtime dependencies, besides scala) – Máté Magyar Jun 13 '17 at 13:03
  • It's not just monads, `f.map(identity) == f` is broken too. As I explained in my answer `map` (usually) creates a subscription and entirely new `Future` – dk14 Feb 25 '18 at 18:44
1

To complement the other points and explain relationship between referential transparency (a requirement) and side-effects (mutation that might break this requirement), here is kinda simplistic but pragmatic view on what's happening:

  • newly created Future immediately submits a Callable task into your pool's queue. Given that queue is a mutable collection - this is basically a side-effect
  • any subscription (from onComplete to map) does the same + uses an additional mutable collection of subscribers per Callable.

Btw, subscriptions are not only in violation of Monad laws as noted by @P.Frolov (for flatMap) - Functor laws f.map(identity) == f are broken too. Especially, in the light of fact that newly created Future (by map) isn't equivalent to original - it has its separate subscriptions and Callable

This "fire and subscribe" allows you to do stuff like:

val f = Future{...}
val f2 = f.map(...)
val f3 = f.map(...)//twice or more

Every line of this code produces a side-effect that might potentially break referential transparency and actually does as many mentioned.

The reason why many authors prefer "referential transparency" term is probably because from low-level perspective we always do some side-effects, however only subset (usually a more high-level one) of those actually makes your code "non-functional".


As per the futures, breaking referential transparency is most disruptive as it also leads to non-determinism (in Futures case):

val f1 = Future {
  println("1")
}

val f2 = Future {
  println("2")
}

It gets worse when this is combined with Monads, including for-comprehension cases mentioned by @Luka Jacobowitz. In practice, monads are used not only to flatten-merge compatible containers, but also in order to guarantee [con]sequential relation. This is probably because even in abstract algebra Monads are generalizing over consequence operators meant as a general characterization of the notion of deduction.

This simply means that it's hard to reason about non-deterministic logic, even harder than just non-referential-transparent stuff:

  • analyzing logs produced by Futures, or even worse actors, is a hell. Even no matter how many labels and thread-local propagation you have - everything breaks eventually.
  • non-deterministic (aka "sometimes appearing") bugs are most annoying and stay in production for years(!) - even extensive high-load testing (including performance tests) doesn't always catch those.

So, even in absence of other criteria, code that is easier to reason about, is essentially more functional and Futures often lead to code that isn't.

P.S. As a conclusion, if your project is tolerant to scalaz/cats/monix/fs2 so on, it's better to use Tasks/Streams/Iteratees. Those libraries introduce some risks of overdesgn of course; however, IMO it's better to spent time simplifying incomprehensible scalaz-code than debugging an incomprehensible bug.

dk14
  • 22,206
  • 4
  • 51
  • 88