Why future has side effects?

前端 未结 4 442
太阳男子
太阳男子 2020-12-16 18:13

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

相关标签:
4条回答
  • 2020-12-16 18:24

    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).

    0 讨论(0)
  • 2020-12-16 18:26

    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.

    0 讨论(0)
  • 2020-12-16 18:31

    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.

    0 讨论(0)
  • 2020-12-16 18:37

    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.

    0 讨论(0)
提交回复
热议问题