在使用Java之前,我对Scala很新,而且大部分时间都是。现在我的代码中都有警告说我应该“避免可变的局部变量”,我有一个简单的问题 - 为什么?
假设我有一个小问题 - 确定四个中的最大值。我的第一个方法是:
def max4(a: Int, b: Int,c: Int, d: Int): Int = {
var subMax1 = a
if (b > a) subMax1 = b
var subMax2 = c
if (d > c) subMax2 = d
if (subMax1 > subMax2) subMax1
else subMax2
}
在考虑此警告消息后,我找到了另一种解决方案:
def max4(a: Int, b: Int,c: Int, d: Int): Int = {
max(max(a, b), max(c, d))
}
def max(a: Int, b: Int): Int = {
if (a > b) a
else b
}
它看起来更漂亮,但这背后的意识形态是什么?
每当我遇到问题时,我都会这样想:“好吧,我们从这开始,然后我们逐步改变事情并得到答案”。我知道问题是我尝试更改一些初始状态以获得答案,并且不明白为什么至少在本地改变事情是不好的?如何在Scala等函数式语言中迭代集合?
就像一个例子:假设我们有一个int列表,如何编写一个函数来返回可被6整除的int的子列表?没有局部可变变量就无法想到解决方案。
答案 0 :(得分:5)
在您的特定情况下,还有另一种解决方案:
def max4(a: Int, b: Int,c: Int, d: Int): Int = {
val submax1 = if (a > b) a else b
val submax2 = if (c > d) c else d
if (submax1 > submax2) submax1 else submax2
}
跟进不是更容易吗?当然我有点偏颇,但我倾向于认为,但不盲目遵循这条规则。如果你看到某些代码可能以可变的方式更易读和简洁地编写,那就这样做吧 - scala的强大之处在于你既不需要既不可变也不可变的方法,你可以在它们之间摆动(顺便说一句同样适用于return
关键字用法。
就像一个例子:假设我们有一个int列表,如何编写一个 返回可被6整除的整数子列表的函数? 没有局部可变变量就无法想到解决方案。
使用递归编写这样的函数当然是可能的,但是,如果可变解决方案看起来很好并且工作正常,为什么不呢?
答案 1 :(得分:5)
它与Scala的关系与一般的函数式编程方法无关。这个想法如下:如果你有常量变量(Java中的final),你可以使用它们而不用担心它们会改变。以同样的方式,您可以并行化代码,而无需担心竞争条件或线程不安全的代码。
在你的例子中并不是那么重要,但想象下面的例子:
val variable = ...
new Future { function1(variable) }
new Future { function2(variable) }
使用最终变量可确保不会出现任何问题。否则,您必须检查主线程以及function1和function2。
当然,如果你不改变它们,可以用可变变量获得相同的结果。但是使用不可变的那些你可以肯定这将是这种情况。
编辑以回答您的修改:
本地变体也不错,这就是你可以使用它们的原因。但是,如果你试图在没有它们的情况下思考方法,你可以像你发布的那样找到解决方案,它更干净,并且可以非常容易地并行化。
如何在Scala等函数式语言中迭代集合?
您可以随时迭代一个不可变的集合,而不会更改任何内容。例如:
val list = Seq(1,2,3)
for (n <- list)
println n
关于你所说的第二件事:你必须停止以传统的方式思考。在函数式编程中,Map,Filter,Reduce等的使用是正常的;以及模式匹配和OOP中不常见的其他概念。对于您给出的示例:
举个例子:假设我们有一个int列表,如何编写一个函数来返回可以被6整除的int的子列表?
val list = Seq(1,6,10,12,18,20)
val result = list.filter(_ % 6 == 0)
答案 2 :(得分:1)
您的两个主要问题:
我会回答这两个问题。
编译器警告不要使用可变局部变量,因为它们经常导致错误。这并不意味着总是如此。但是,您的示例代码几乎是一个典型的示例,其中可变本地状态的使用完全不必要,其方式不仅使其更容易出错且更不清晰,而且效率也更低。
您的第一个代码示例比您的第二个功能解决方案效率更低。当您只需要分配一个时,为什么可能会对submax1
进行两次分配?你问两个输入中哪一个更大,所以为什么不先问 再做一个作业呢?为什么你的第一个方法只是在问这么简单的问题的过程中暂时存储部分状态?
由于不必要的代码重复,您的第一个代码示例也效率低下。你反复询问&#34;这是两个价值中最大的一个?&#34;为什么要独立写出3次代码?不必重复代码是 OOP 中的一个已知坏习惯,与 FP 一样多,并且出于完全相同的原因。每次不必要地重复代码时,都会打开潜在的错误来源。添加可变的本地状态(特别是在不必要的情况下)只会增加脆弱性和难以发现错误的可能性,即使在短代码中也是如此。您只需在一个位置键入submax1
而不是submax2
,您可能暂时不会注意到该错误。
您的第二个 FP 解决方案会删除代码重复,从而大大降低出错的可能性,并显示根本不需要可变的本地状态。正如您自己所说,它也更清晰,更清晰 - 并且比om-nom-nom的答案中的替代解决方案更好。
(顺便说一下,写这么简单函数的惯用Scala方法是
def max(a: Int, b: Int) = if (a > b) a else b
哪种terser风格强调其简洁性并使代码更简洁)
你的第一个解决方案是低效且脆弱的,但这是你的第一直觉。警告使您找到更好的解决方案。警告证明了它的价值。 Scala旨在让Java开发人员可以访问,并且被许多人采用,具有长期的命令式风格,很少或根本不了解 FP 。他们的第一直觉几乎总是和你的一样。您已经演示了该警告如何帮助改进代码。
有 的情况下,使用可变的本地状态可能会更快,但Scala专家(不仅仅是纯粹的 FP 真信徒)的建议是{{3只有在有明确使用的情况下才能达到可变性。这与许多开发人员的本能相反,即使对经验丰富的Scala开发人员感到烦恼,警告仍然有用。
有趣的是,某种 max 功能经常出现在FP / Scala&#34;的问题。提问者经常绊倒他们prefer immutability引起的错误......这两个链接都表明了某些开发人员对可变状态的常常迟钝上瘾,同时也引导我进入你的另一个问题。
在Scala中有三种函数方法可以迭代集合
你的问题:
假设我们有一个int列表,如何编写一个返回可被6整除的int的子列表的函数?如果没有局部可变变量,就无法想到解决方案
答案:假设xs
是整数的列表(或其他序列),那么
for (x <- xs; if x % 6 == 0) yield x
将为您提供一个序列(与xs
类型相同),仅包含可被6整除的项目(如果有)。不需要可变状态。 Scala会为您迭代序列并返回符合您条件的任何内容。
如果你还没有学会理解的力量(也称为use of local state),你真的应该这样做。它是Scala语法中非常富有表现力和强大的一部分。如果你愿意,你甚至可以使用副作用和可变状态(查看我刚刚链接到的教程的最后一个例子)。也就是说,可能有sequence comprehensions并且它们被一些开发人员过度使用。
在第一部分末尾的问题I unexpected performance penalties中,我在答案中给出了一个非常简单明确的递归解决方案,用于从列表中返回最大的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 )
}
我不打算解释模式匹配和显式递归是如何工作的(阅读我的其他答案或linked to)。我只是告诉你这项技术。大多数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)
}
我称之为天真,因为它不是this one因此可能会使你的筹码受到打击。可以使用累加器列表和内部辅助函数编写更安全的版本,但我将该练习留给您。无论你如何尝试,结果都会比天真的版本更不漂亮,但是努力是有教育意义的。
递归是一项非常重要的学习技巧。也就是说,一旦你学会了这么做,下一个要学习的重点是你通常可以避免明确地使用它...
您是否注意到我的两个显式递归示例有多相似?这是因为列表上的大多数递归具有相同的基本结构。如果你写了很多这样的函数,你会多次重复这个结构。这使它成为样板;浪费你的时间和潜在的错误来源。
现在,有许多复杂的方法可以解释tail recursive,但是一个简单的概念就是它们从传递中取出了样板。他们为您处理递归和累加器值的管理。他们所要求的是你为累加器提供种子值以及在每次迭代时应用的函数。
例如,这里有一种使用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设计师会提供一个更简单的folds,它使用折叠在引擎盖下实现。想再次找到那些可分割的六个Ints吗?
xs.foldRight(Nil: List[Int]) {(x, acc) => if (x % 6 == 0) x :: acc else acc}
以一个空列表作为累加器开始,迭代每个项目,只将可被6整除的那些添加到累加器。同样,为您提供了一个更简单的基于折叠的 HoF :
xs filter { _ % 6 == 0 }
折叠和相关的高阶函数比理解或显式递归更难理解,但是非常强大和富有表现力(对于理解它们的任何其他人)。它们消除了样板,消除了潜在的错误来源。因为它们是由核心语言开发人员实现的,所以它们可以更高效(并且随着语言的进展,实现可以改变,而不会破坏您的代码)。经验丰富的Scala开发人员使用它们优先于理解或显式递归。
答案 3 :(得分:0)
首先,您可以像这样重写您的示例:
def max(first: Int, others: Int*): Int = {
val curMax = Math.max(first, others(0))
if (others.size == 1) curMax else max(curMax, others.tail : _*)
}
这使用varargs和尾递归来查找最大的数字。当然,还有很多其他方法可以做同样的事情。
回答你的问题 - 这是一个很好的问题,也是我第一次开始使用scala时想到的问题。就个人而言,我认为整个不可变/函数式编程方法有点夸大其词。但是,它的价值在于支持它的主要论点:
不可变代码更易于阅读(主观)
不可变代码更强大 - 改变可变状态可能会导致错误,这当然是正确的。以此为例:
for (int i=0; i<100; i++) {
for (int j=0; j<100; i++) {
System.out.println("i is " + i = " and j is " + j);
}
}
这是一个过于简化的示例,但它仍然很容易错过错误,编译器无法帮助您
可变代码通常不是线程安全的。即使是琐碎的,看似原子的操作也不安全。以i++
为例,这看起来像一个原子操作,但它实际上相当于:
int i = 0;
int tempI = i + 0;
i = tempI;
不可变数据结构不允许您执行此类操作,因此您需要明确考虑如何处理它。当然,正如您所指出的,局部变量通常是线程安全的,但并不能保证。可以将ListBuffer实例变量作为参数传递给方法,例如
然而,不可变和函数式编程风格存在缺点:
<强>性能即可。它在编译和运行时通常都比较慢。编译器必须强制执行不变性,并且JVM必须分配比可变数据结构所需的更多对象。对于收藏品尤其如此。
大多数scala示例都显示类似val numbers = List(1,2,3)
的内容,但在现实世界中,硬编码值很少见。我们通常动态地构建集合(从数据库查询等)。虽然scala可以在集合中重新分配值,但每次修改它时它仍必须创建一个新的集合对象。如果要向scala List(不可变)添加1000个元素,JVM将需要分配(然后GC)1000个对象
难以维护。功能代码可能非常难以阅读,看到这样的代码并不罕见:
val data = numbers.foreach(_.map(a => doStuff(a).flatMap(somethingElse)).foldleft("", (a : Int,b: Int) => a + b))
我不了解你,但我发现这类代码真的难以理解!
难以调试。功能代码也很难调试。尝试在我上面的(可怕的)示例中间设置一个断点
我的建议是使用功能性/不可变的风格,它真正有意义,你和你的同事都很自在。不要使用不可变结构,因为它们很酷或它很聪明&#34;。复杂且具有挑战性的解决方案将为您带来Uni的奖励积分,但在商业领域,我们需要简单的解决方案来解决复杂问:)
答案 4 :(得分:0)
使用不可变变量总是更好,因为它们使您的代码更易于阅读。编写递归代码可以帮助您解决问题。
def max(x: List[Int]): Int = {
if (x.isEmpty == true) {
0
}
else {
Math.max(x.head, max(x.tail))
}
}
val a_list = List(a,b,c,d)
max_value = max(a_list)