您的两个主要问题:
- 为什么要针对本地状态变化发出警告?
- 如何在没有可变状态的情况下迭代集合?
我都会回答。
警告
编译器警告不要使用可变局部变量,因为它们经常会导致错误。这并不意味着情况总是如此。但是,您的示例代码几乎是一个完全不必要地使用可变本地状态的经典示例,这种方式不仅使其更容易出错且不太清晰,而且效率也较低。
您的第一个代码示例比您的第二个功能解决方案效率低。当您只需要分配一个时,为什么还要对submax1 进行两个分配?你问两个输入中哪一个更大,那么为什么不先问,然后再做一个赋值呢?为什么您的第一个方法只是在提出这样一个简单问题的过程中才临时存储部分状态?
由于不必要的代码重复,您的第一个代码示例也效率低下。您反复问“两个值中哪个是最大的?”为什么要独立写出那 3 次的代码呢? OOP 和 FP 一样,不必要地重复代码是众所周知的坏习惯,原因完全相同。每次不必要地重复代码时,都会打开潜在的错误源。添加可变的本地状态(尤其是在不必要的情况下)只会增加脆弱性和难以发现错误的可能性,即使在短代码中也是如此。您只需在一个地方输入submax1 而不是submax2,您可能暂时不会注意到错误。
您的第二个,FP 解决方案删除了代码重复,大大降低了出错的机会,并表明根本不需要可变的本地状态。正如您自己所说,它也比 om-nom-nom 答案中的替代解决方案更干净、更清晰。
(顺便说一句,编写这么简单的函数的惯用 Scala 方式是
def max(a: Int, b: Int) = if (a > b) a else b
哪种更简洁的风格强调其简单性并使代码不那么冗长)
您的第一个解决方案效率低下且脆弱,但这是您的第一直觉。该警告使您找到了更好的解决方案。警告证明了它的价值。 Scala 被设计成可供 Java 开发人员使用,并且被许多具有命令式风格的长期经验和很少或根本不了解 FP 的人所采用。他们的第一直觉几乎总是和你一样。您已经演示了该警告如何帮助改进代码。
在的情况下,使用可变的本地状态可能会更快,但 Scala 专家(不仅仅是纯粹的 FP 真正的信徒)的建议是 prefer immutability并且只有在有明确使用情况的情况下才能实现可变性。这违背了许多开发人员的本能,即使对有经验的 Scala 开发人员来说,警告也是有用的。
有趣的是,某种 max 函数在“FP/Scala 新手”问题中出现的频率很高。提问者经常因他们的use of local state... 引起的错误而绊倒,该链接既表明了一些开发人员对可变状态的经常迟钝上瘾,同时也将我引向了您的其他问题。
集合的函数迭代
在 Scala 中迭代集合的三种函数式方法
- 供理解
- 显式递归
- 折叠和其他高阶函数
供理解
你的问题:
假设我们有一个整数列表,如何编写一个返回可被 6 整除的整数子列表的函数?想不出没有局部可变变量的解决方案
答案:假设xs 是一个整数列表(或其他一些序列),那么
for (x <- xs; if x % 6 == 0) yield x
会给你一个序列(与xs 的类型相同),其中只包含那些可以被 6 整除的项目,如果有的话。不需要可变状态。 Scala 只是为您遍历序列并返回符合您条件的任何内容。
如果您还没有学习 for comprehensions(也称为 sequence comprehensions)的强大功能,那么您真的应该学习。它是 Scala 语法中非常有表现力和强大的部分。如果需要,您甚至可以将它们与副作用和可变状态一起使用(查看我刚刚链接到的教程的最后一个示例)。也就是说,可能有unexpected performance penalties,它们被一些开发人员过度使用。
显式递归
在第一部分末尾的问题 I linked to 中,我在回答中给出了一个非常简单、明确的递归解决方案,以从列表中返回最大的 Int。
def max(xs: List[Int]): Option[Int] = xs match {
case Nil => None
case List(x: Int) => Some(x)
case x :: y :: rest => max( (if (x > y) x else y) :: rest )
}
我不打算解释模式匹配和显式递归是如何工作的(阅读我的其他答案或this one)。我只是向你展示技术。大多数 Scala 集合可以递归迭代,而不需要可变状态。如果您需要跟踪您在此过程中所做的事情,您可以传递一个累加器。 (在我的示例代码中,我将累加器放在列表的前面以保持代码更小,但查看这些问题的其他答案以更常规地使用累加器)。
但这是一种(简单的)显式递归方法,可以找到可被 6 整除的整数
def divisibleByN(n: Int, xs: List[Int]): List[Int] = xs match {
case Nil => Nil
case x :: rest if x % n == 0 => x :: divisibleByN(n, rest)
case _ :: rest => divisibleByN(n, rest)
}
我称它为幼稚,因为它不是tail recursive,因此可能会炸毁你的筹码。可以使用累加器列表和内部辅助函数编写更安全的版本,但我把这个练习留给你。无论您如何尝试,结果都不会像原始版本那样漂亮,但这种努力是有教育意义的。
递归是一种非常重要的学习技术。也就是说,一旦你学会了这样做,接下来要学习的重要一点是,你通常可以避免自己明确地使用它......
折叠和其他高阶函数
您是否注意到我的两个显式递归示例有多相似?这是因为列表上的大多数递归都具有相同的基本结构。如果你写了很多这样的函数,你会重复这个结构很多次。这使它成为样板;浪费您的时间和潜在的错误来源。
现在,有许多复杂的方法可以解释folds,但一个简单的概念是它们将样板代码从递归中取出。他们为您处理累加器值的递归和管理。他们所要求的只是您为累加器提供一个种子值以及在每次迭代时应用的函数。
例如,这是一种使用 fold 从列表中提取最高 Int 的方法xs
xs.tail.foldRight(xs.head) {(a, b) => if (a > b) a else b}
我知道你不熟悉折叠,所以这对你来说可能是胡言乱语,但你肯定认出了我在右边传递的 lambda(匿名函数)。我在那里做的是获取列表中的第一项(xs.head)并将其用作累加器的种子值。然后我告诉列表的其余部分 (xs.tail) 对其自身进行迭代,依次将每个项目与累加器值进行比较。
这种事情很常见,所以Collections api设计者提供了一个简写版本:
xs.reduce {(a, b) => if (a > b) a else b}
(如果您查看源代码,您会发现他们使用折叠实现了它)。
您可能想要对 Scala 集合迭代地执行的任何操作都可以使用折叠来完成。通常,api 设计者会提供一个更简单的higher-order function,它在后台使用折叠来实现。想再次找到那些可被 6 整除的整数吗?
xs.foldRight(Nil: List[Int]) {(x, acc) => if (x % 6 == 0) x :: acc else acc}
从一个空列表作为累加器开始,迭代每个项目,只将那些能被 6 整除的项目添加到累加器中。同样,我们为您提供了一个更简单的基于折叠的 HoF:
xs filter { _ % 6 == 0 }
折叠和相关的高阶函数比推导式或显式递归更难理解,但非常强大且富有表现力(对于任何理解它们的人来说)。它们消除了样板,消除了潜在的错误来源。因为它们是由核心语言开发人员实现的,所以它们可以更高效(并且随着语言的进步,实现可以改变,而不会破坏你的代码)。经验丰富的 Scala 开发人员优先使用它们来进行推导式或显式递归。
tl;博士
- 为理解而学习
- 学习显式递归
- 如果高阶函数可以完成这项工作,请不要使用它们。