【问题标题】:Why future has side effects?为什么未来会有副作用?
【发布时间】:2017-05-26 07:33:48
【问题描述】:

我正在阅读这本书FPiS,在第 107 页作者说:

我们应该注意到,Future 没有纯粹的函数式接口。 这是我们不希望我们图书馆的用户使用的部分原因 直接处理Future。但重要的是,即使方法 未来依赖副作用,我们的整个 Par API 保持纯净。它的 只有在用户调用 run 并且实现收到一个 ExecutorService,我们暴露了 Future 机器。我们的用户 因此编程到一个纯接口,其实现 尽管如此,最终还是依赖于效果。但是自从我们的 API 保持纯净,这些影响不是副作用。

为什么Future没有纯粹的功能接口?

【问题讨论】:

标签: scala functional-programming


【解决方案1】:

问题在于,由于 Future 的急切本性,创建一个会引发副作用的 Future 本身也是一种副作用。

这破坏了引用透明度。 IE。如果您创建一个仅打印到控制台的 Future,则 Future 将立即运行并运行副作用,而无需您要求它。

一个例子:

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

这会导致“Foo”被打印两次。现在如果Future 是引用透明的,我们应该能够在下面的非内联版本中得到相同的结果:

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

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

但是,这只会打印一次“Foo”,而且问题更大,无论您是否包含for-expression,它都会打印它。

通过引用透明的表达式,我们应该能够在不改变程序语义的情况下内联任何表达式,Future 不能保证这一点,因此它破坏了引用透明性并且本质上是有效的。

【讨论】:

  • 我看不出这对未来有什么特殊意义。任何执行函数的表达式都不是这样吗?如果你给它一个带有副作用的 lambda,它就会产生副作用......(Duh!):) 就像......我不知道...... Seq(1,2,3).filter { println _; true } 这是否使Seq 也不是纯粹的功能?
  • 确实如此。但大多数情况下,这就是 Future 的使用方式。如果您将Future 与纯计算一起使用,那么它应该被认为是引用透明的。但是,大多数人不会那样使用它。如果将它与 Monix 任务或 FS2 任务进行比较,它可以保证可以捕获副作用的值的引用透明性。 IE。如果您尝试对Task 执行相同操作,则两个代码 sn-ps 的行为方式都相同 :)
  • 好的...你能解释一下Task到底有什么更好的地方吗?我的意思是,你可以内联val task = Task delay { prinltn("foo") },好的,但是task.run 仍然不是引用透明的。这不就是把问题往下推吗?
  • 是的,确实如此!函数式编程就是将副作用推到程序的边缘! :) 看看这篇文章:typelevel.org/blog/2017/05/02/io-monad-for-cats.html
  • 这是一个不好的理由说 Future 是副作用,因为如果这是真的,你需要说 scala 中的所有函数都是副作用。仅仅因为你可以在其中调用副作用函数,并不意味着它本身就有副作用。
【解决方案2】:

FP 的一个基本前提是referential transparency。换句话说,避免副作用。

什么是副作用?来自Wikipedia

在计算机科学中,如果函数或表达式修改了其范围之外的某些状态,或者与其调用函数或外部世界有可观察的交互,则称该函数或表达式具有副作用。 (除了按约定返回值:返回值对调用函数有影响,但这通常不被视为副作用。)

什么是 Scala 的未来?来自documentation page

Future 是一个占位符对象,表示可能尚不存在的值。

因此,未来可以从尚不存在的值转换为现有值,而无需与程序的其余部分进行任何交互或与程序的其余部分进行任何交互,并且正如您所引用的:“Future 上的方法依赖于副作用。”

看起来 Scala 期货不保持引用透明度。

【讨论】:

    【解决方案3】:

    据我所知,Future 在创建时会自动运行其计算。即使它在嵌套计算中没有副作用,它仍然违反flatMap 组合规则,因为它会随着时间改变状态:

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

    抛开平等实现问题,我们可以在这里有一个竞争条件:新的Future 立即运行一小部分时间,如果它已经完成,它的isCompleted 可能与someFuture 不同。

    为了纯粹的w.r.t。它所代表的效果,Future 应该推迟其计算并仅在明确要求时运行它,例如 Par(或 scalazTask)。

    【讨论】:

    • 这是正确答案。虽然,据我所知,Task 仍然有副作用,只是在明确要求的一点上。我不确定你是否可以在没有副作用的情况下实际做到这一点。当然你可以连接计算,但是如果你没有副作用,你需要在某个时候阻塞,这是相当不可取的。
    • 但这并不是一个大问题 - 在 FP 程序中运行 Task 的预期位置是它的入口点,这应该是你在那里实际做的最后一件事。这正是程序的所有部分可以保持可组合性的方式,包括 I/O 操作——它们被组装成通用的执行计划,以便在最后运行。
    • 公平点!有没有没有 scalaz 包袱实现类似任务的好库?
    • 我真的不知道每个人都在谈论的scalaz 的“包袱”到底是什么(数学命名是相当标准的,而不是 Haskell 特定的),但似乎有相对较新的 Monix 库做同样的事情:monix.io/docs/2x/eval/task.html
    • 谢谢!我并不是说它是质量的包袱,但它的大小和范围很大,我不想将它包含在我相对较小的库中(现在它没有任何运行时依赖项,除了 scala)跨度>
    【解决方案4】:

    为了补充其他观点并解释引用透明度(一项要求)和副作用(可能会破坏此要求的突变)之间的关系,以下是对正在发生的事情的一种简单但务实的看法:

    • 新创建的Future 立即将Callable 任务提交到池的队列中。鉴于队列是一个可变集合 - 这基本上是一个副作用
    • 任何订阅(从onCompletemap)都会这样做 + 每个Callable 使用一个额外的可变订阅者集合。

    顺便说一句,订阅不仅违反了 @P.Frolov (for flatMap) 指出的 Monad 法则 - 函子法则 f.map(identity) == f 也被打破。特别是,鉴于新创建的Futuremap)并不等同于原始 - 它有单独的订阅和Callable

    这种“触发并订阅”允许您执行以下操作:

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

    这段代码的每一行都会产生一个副作用,可能会破坏引用透明度,并且实际上会像前面提到的那样做。

    许多作者更喜欢“引用透明”一词的原因可能是因为从低级别的角度来看,我们总是会产生一些副作用,但是只有其中的子集(通常是更高级别的)实际上使您的代码“非-功能”。


    根据未来,打破引用透明度最具破坏性,因为它还会导致不确定性(在Futures 的情况下):

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

    当它与 Monads 结合使用时,情况会变得更糟,包括 @Luka Jacobowitz 提到的 for-comprehension 案例。在实践中,monad 不仅用于flatten-merge 兼容的容器,还用于保证 [con] 顺序关系。这可能是因为即使在抽象代数中,Monads 也在对作为演绎概念的一般表征的结果运算符进行泛化。

    这只是意味着很难推理关于非确定性逻辑,甚至比非引用透明的东西更难:

    • 分析Futures 甚至更糟糕的演员生成的日志简直是地狱。即使你有多少标签和线程局部传播——一切最终都会中断。
    • 非确定性(又名“有时会出现”)错误是最烦人的,并且会在生产环境中持续数年(!) - 即使是广泛的高负载测试(包括性能测试)也不一定能捕捉到这些错误。

    因此,即使没有其他标准,更容易推理的代码本质上也更具功能性,而Futures 通常会导致无法推理的代码。

    附:作为结论,如果你的项目能够容忍scalaz/cats/monix/fs2等,最好使用Tasks/Streams/Iteratees。这些库当然会引入一些过度设计的风险;但是,IMO 最好花时间简化难以理解的 scalaz 代码,而不是调试难以理解的错误。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-15
      • 2012-05-29
      • 2020-09-09
      • 2010-12-20
      • 1970-01-01
      • 2021-10-12
      相关资源
      最近更新 更多