为了补充其他观点并解释引用透明度(一项要求)和副作用(可能会破坏此要求的突变)之间的关系,以下是对正在发生的事情的一种简单但务实的看法:
- 新创建的
Future 立即将Callable 任务提交到池的队列中。鉴于队列是一个可变集合 - 这基本上是一个副作用
- 任何订阅(从
onComplete 到map)都会这样做 + 每个Callable 使用一个额外的可变订阅者集合。
顺便说一句,订阅不仅违反了 @P.Frolov (for flatMap) 指出的 Monad 法则 - 函子法则 f.map(identity) == f 也被打破。特别是,鉴于新创建的Future(map)并不等同于原始 - 它有单独的订阅和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 代码,而不是调试难以理解的错误。