【问题标题】:Immutable state in FPFP 中的不可变状态
【发布时间】:2023-03-31 05:35:01
【问题描述】:

这个问题是在学习一些关于 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 循环递增累加器。原因是这个累加器是一个可变变量。

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

编辑:

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

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

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

【问题讨论】:

  • 关于 1) 的重点是,如果您的数据结构首先是不可变的,则您不需要返回副本。
  • 索引变量通常不会暴露,你是对的。只要我们谈论仅用于迭代的琐碎索引,我们就可以保存。这仍然不仅仅是一个最佳实践,因为这是您可以安全使用全局索引并且仍然可以保存线程的唯一方法。
  • 回答您的最后一个问题:def m() = { var x = 1; ... (20 lines of some code); x }。您需要扫描多少行才能知道返回的内容? (提示:20+)如果是 val x = 1 呢?

标签: scala functional-programming immutability


【解决方案1】:

在 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

现在回到您最初的问题:为什么不可变变量更易于阅读和推理?这可能会让人感到意外,但程序员也只是人类。对于具有许多活动部件的系统,人类是不好的推理。不变性减少了移动部件的数量,从而更容易推理程序。

【讨论】:

    【解决方案2】:

    你说得对,如果引用没有转义函数,那么不变性并不是最关心的问题,但还有其他考虑因素。命令式循环具有非常通用和不精确的语义,因此大多数人在学习替代方案后就会发现它们太草率了。

    例如,命令式循环没有返回类型。不检查其参数的输入类型。许多类型的命令式循环迫使程序员不必要地处理与问题陈述无关的索引。只有少数几种命令式循环旨在涵盖所有可能的情况,因此它们过于通用。您无法创建自己的新型命令式循环来更好地适合您的特定情况。它们几乎总是必须内置到语言本身中。

    出于某种奇怪的原因,似乎每个人都在递归和命令式循环之间做出了错误的选择,但递归确实是函数式程序员最差的工具。我们避免使用它,除非绝对必要,或者除非算法自然递归地表达。有许多高阶函数可用于比递归更精确地表达大多数日常代码,同时保留所有优点,例如 Martjin 的 foldLeft 示例。

    使用foldLeft 而不是for 循环,单是类型检查的好处就值得。空列表可以轻松处理。没有奇怪的累加器必须使用循环外的范围进行初始化。你不必为那些你根本不关心的临时变量想出奇怪的名字。它只是更清洁。

    您可能会说这是一个特殊的简单案例,但要小心,避免命令式循环可以在绝大多数情况下为您提供这种体验,而在递归最终变得复杂的情况下,命令式解决方案通常会一样复杂。当您对所有替代方案进行真正的比较时,命令式循环只会因为人们熟悉它们而获得好评。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-11-12
      • 2018-11-06
      • 2022-01-10
      • 2020-10-11
      • 2021-03-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多