Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why future has side effects?

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?

like image 470
softshipper Avatar asked May 26 '17 07:05

softshipper


3 Answers

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.

like image 165
Luka Jacobowitz Avatar answered Oct 10 '22 22:10

Luka Jacobowitz


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.

like image 40
dk14 Avatar answered Oct 10 '22 22:10

dk14


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.

like image 35
jwvh Avatar answered Oct 10 '22 22:10

jwvh