在关注Scala的一些教程时会想到这个问题,但我认为在函数式编程方面,这个问题很有意思。
我不确定FP中不变性的重要性。我可以想到两种不同的情况:
1)类方法不返回实际字段,而是返回它们的副本。例如,如果我们有一个我们需要保持不可变的类Dog,那么它的功能是:
getToys() { return new ArrayList(this.toys); }
而不是:
getToys() { return this.toys; }
这种情况对我来说很有意义,在第二种情况下,客户端代码实际上可能会破坏对象。我的怀疑主义在于第二种情况:
2)在Scala和大多数其他FP语言中,我们更喜欢递归调用:
sum(x: List[Int], acc: Int) {
if(x.isEmpty) return acc;
sum(x.tail, acc + x.head);
}
反对传统的for循环递增累加器。原因是这个累加器是一个可变变量。
该变量永远不会暴露在函数之外,所以为什么要注意使它变得不可变?
修改
似乎最重要的最佳做法是参考透明度 而且不是严格的不变性,大致意味着我们不关心可变状态,只要它不被客户端代码发现。然而,人们仍然声称,即使可变状态影响局部变量(如我的第一个例子),代码更易读或更容易推理。
就个人而言,我认为循环比递归更具可读性。
所以真正的问题是:为什么使用不可变变量的代码更易于阅读/推理?
答案 0 :(得分:4)
在Scala和大多数其他FP语言中,我们更喜欢递归调用...而不是传统的for循环递增累加器。原因是这个累加器是一个可变变量。
您假设递归优先于递增累加器的传统循环。我同意函数式程序员喜欢递归,但不是因为你认为的原因。在许多情况下,递归只是表达问题的一种自然而简洁的方式。请注意,由于尾调用优化消除了分配新堆栈帧的开销,因此性能不是针对递归的参数。
您将递归与递增累加器的for循环进行了比较。大多数程序员更喜欢在没有索引的情况下循环一个集合(例如Scala中的for (i <- 0 to 10) { .. }
或Java中的for (T x : collection) { .. }
。for
构造在一个集合上进行迭代抽象,使得a)更多成为可能清楚地传达您的意图和b)降低错误的风险(例如一次性错误)。进一步抽象,我们可以表达你使用高阶折叠求和整数列表的例子:
scala> (1 to 10).foldLeft(0)(_+_)
res0: Int = 55
现在回到原来的问题:为什么不可变变量更容易阅读和推理?这可能是一个惊喜,但程序员也只是人类。关于具有许多活动部件的系统,人类是不好的理由。不变性减少了运动部件的数量,使得更容易推理程序。
答案 1 :(得分:1)
你是对的,如果引用并没有逃避这个功能,那么不变性并不是最大的问题,但还有其他的考虑因素。命令式循环具有非常通用和不精确的语义,因此大多数人一旦了解了替代方案,就会发现它们过于草率。
例如,命令式循环没有返回类型。不检查其参数的输入类型。许多类型的命令循环迫使程序员不必要地处理与问题陈述无关的索引。只有少数几种命令性循环可以涵盖所有可能的情况,因此它们过于通用。您不能创建自己的新型强制循环,以更好地适应您的特定情况。它们几乎总是必须用语言本身构建。
由于一些奇怪的原因,每个人似乎都在递归和命令循环之间做出了错误的选择,但递归实际上是函数式程序员带中最糟糕的工具。除非绝对必要,否则我们会避免它,或者除非算法以递归方式自然表达。有许多高阶函数可用于比递归更精确地表达大多数日常代码,同时保留所有好处,如Martjin的foldLeft
示例。
单独使用类型检查功能值得使用foldLeft
而不是for
循环。空列表可以毫不费力地处理。没有奇怪的累加器必须使用循环外的作用域进行初始化。您不必为所有那些您不关心的临时变量提出奇怪的名称。它只是简单清洁。
你可能会说这是一个特殊的简单案例,但是要小心谨慎,避免命令式循环可以在绝大多数情况下为你提供这种体验,而递归最终会变得复杂,必要的解决方案通常会有同样复杂。当你对所有替代方案进行真正的比较时,命令式循环才会有利,因为人们熟悉它们。