FP 中的不可变状态

pil*_*ilu 0 functional-programming scala immutability

在学习有关 Scala 的一些教程时,我想到了这个问题,但我认为一般来说,在函数式编程方面很有趣。

我不确定 FP 中不变性的重要性。我可以想到两种不同的情况:

1) 类方法不返回实际字段,而是返回它们的副本。例如,如果我们有一个需要保持不可变的类 Dog,那么它的功能:

getToys() { return new ArrayList(this.toys); }
Run Code Online (Sandbox Code Playgroud)

代替:

getToys() { return this.toys; }
Run Code Online (Sandbox Code Playgroud)

这种情况对我来说很有意义,在第二种情况下,客户端代码实际上可能会损坏对象。我的怀疑在于第二种情况:

2) 在 Scala 和大多数其他 FP 语言中,我们更喜欢递归调用:

sum(x: List[Int], acc: Int) { 
  if(x.isEmpty) return acc;
  sum(x.tail, acc + x.head); 
}
Run Code Online (Sandbox Code Playgroud)

与传统的 for 循环递增累加器相反。原因是这个累加器是一个可变变量。

那个变量永远不会暴露在函数之外,所以为什么要让它不可变呢?

编辑:

似乎最重要的最佳实践是引用透明性而不是严格的不变性,这意味着我们不关心可变状态,只要它不能被客户端代码发现。然而,人们仍然声称,即使可变状态影响局部变量(如我的第一个示例),代码也更具可读性或更易于推理。

就个人而言,我认为循环比递归更具可读性。

所以真正的问题是:为什么使用不可变变量的代码更容易阅读/推理?

Mar*_*ijn 5

在 Scala 和大多数其他 FP 语言中,我们更喜欢递归调用......而不是传统的 for 循环递增累加器。原因是这个累加器是一个可变变量。

您假设递归优于传统的递增累加器的循环。我同意函数式程序员喜欢递归,但不是因为你认为的原因。在很多情况下,递归只是表达问题的一种自然而简洁的方式。请注意,由于尾调用优化消除了分配新堆栈帧的开销,因此性能不是反对递归的论据。

您将递归与递增累加器的 for 循环进行了比较。大多数程序员更喜欢在没有索引的情况下循环遍历集合(例如for (i <- 0 to 10) { .. }在 Scala 或for (T x : collection) { .. }Java 中。for构造抽象而不是遍历集合,从而可以 a)更清楚地传达您的意图和 b)降低出错的风险(例如一次性错误)。进一步抽象,我们可以表达您使用高阶折叠对整数列表求和的示例:

scala> (1 to 10).foldLeft(0)(_+_)
res0: Int = 55
Run Code Online (Sandbox Code Playgroud)

现在回到你最初的问题:为什么不可变变量更容易阅读和推理?这可能令人惊讶,但程序员也只是人类。人类对具有许多活动部件的系统的推理很糟糕。不变性减少了移动部件的数量,从而更容易对程序进行推理。