【Scala原理系列】 foldLeft 由来原理用法示例详解
由来
在Scala的方法中,可以有多个参数列表。这种特性允许我们将参数分组,并以不同的方式使用和传递它们。
在Scala集合API中的Iterable特质中定义了一个具有多参数列表的方法foldLeft:
foldLeft
是 Scala 集合类的一个高阶函数,用于对集合中的元素进行累积计算。它的原理基于函数式编程中的折叠(fold)操作。
foldLeft
的原理是通过遍历集合中的元素,逐步将每个元素应用于指定的累积函数,并将结果传递给下一个元素,最终得到一个最终的累积结果。
trait Iterable[A]:
...
def foldLeft[B](z: B)(op: (B, A) => B): B
...
foldLeft
方法将一个两个参数的函数op
应用于初始值z
和该集合的所有元素,从左到右进行计算。
以下是其使用示例:
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val res = numbers.foldLeft(0)((m, n) => m + n)
println(res) // 55
使用场景
多参数列表的建议使用场景包括:
1. 推断类型:
在Scala中,类型推断按照每个参数列表进行。假设你有以下方法:
def foldLeft1[A, B](as: List[A], b0: B, op: (B, A) => B) = ???
你可能希望以以下方式调用它,但会发现它无法编译通过:
def notPossible = foldLeft1(numbers, 0, _ + _)
你需要像以下方式之一调用它:
def firstWay = foldLeft1[Int, Int](numbers, 0, _ + _)
def secondWay = foldLeft1(numbers, 0, (a: Int, b: Int) => a + b)
这是因为Scala无法推断出函数_ + _
的类型,因为它仍在推断A
和B
。通过将参数op
移动到自己的参数列表中,A
和B
会在第一个参数列表中进行推断。这些推断的类型将在第二个参数列表中可用,_ + _
将与推断的类型(Int, Int) => Int
匹配。
2. 隐式参数
要将特定参数指定为隐式参数,必须将它们放置在它们自己的隐式参数列表中。例如:
def execute(arg: Int)(implicit ec: scala.concurrent.ExecutionContext) = ???
3. 偏应用函数:
当使用较少数量的参数列表调用方法时,它将产生一个以缺失的参数列表作为其参数的函数。这被称为偏应用函数。例如:
val numbers = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val numberFunc = numbers.foldLeft(List[Int]()) _
val squares = numberFunc((xs, x) => xs :+ x*x)
println(squares) // List(1, 4, 9, 16, 25, 36, 49, 64, 81, 100)
val cubes = numberFunc((xs, x) => xs :+ x*x*x)
println(cubes) // List(1, 8, 27, 64, 125, 216, 343, 512, 729, 1000)
不适合的场景
虽然 foldLeft
是一个非常有用的函数,但并不适用于所有场景。以下是一些可能不适合使用 foldLeft
的情况:
-
需要并行计算:
foldLeft
是一种顺序计算的操作,每次迭代都依赖前一次迭代的结果。如果您需要在并行环境下执行计算,可以考虑使用其他支持并行操作的函数,如fold
或reduce
。 -
对集合进行早期终止:
foldLeft
会遍历整个集合并计算最终累积值,无法在中途提前结束。如果您希望在某些条件下提前终止计算,那么使用foldLeft
可能不是最合适的选择。可以考虑使用其他函数,如takeWhile
或find
来实现这种需求。 -
不需要累积结果:如果您只关注集合中的某些元素,而不需要累积结果,那么使用
foreach
更加直接和简洁。foreach
函数可以对集合中的每个元素应用一个函数,而无需显式地进行累积操作。 -
性能要求较高的大型数据集:尽管
foldLeft
在处理小型数据集时效果很好,但对于大型数据集,由于每次迭代都会产生新的累积值,可能会导致性能下降。在这种情况下,可以考虑使用更高效的聚合操作,如fold
或reduce
。
总之,尽管 foldLeft
是一个功能强大的函数,但在某些特定场景下可能不是最佳选择。在选择适当的函数时,要根据具体需求和性能要求进行评估和比较,并选择最合适的函数来满足您的目标。
与"柯里化"的比较
有时你可能会看到具有多个参数列表的方法被称为“柯里化”。
柯里化是将一个接受多个参数的函数转换为一系列每次只接受一个参数的函数的技术。
我们不鼓励使用“柯里化”一词来指代Scala中的多参数列表,原因有两点:
-
在Scala中,多参数和多参数列表是直接指定和实现的,而不是从单参数函数派生出来的。
-
这可能会与Scala标准库的curried和uncurried方法产生混淆,这些方法根本不涉及多参数列表。
尽管如此,在多参数列表和柯里化之间确实存在相似之处。尽管在定义时它们是不同的,但在调用时可能看起来完全相同,就像下面的例子一样:
// 使用多参数列表的版本
def addMultiple(n1: Int)(n2: Int) = n1 + n2
// 两种不同的方式得到柯里化版本
def add(n1: Int, n2: Int) = n1 + n2
val addCurried1 = (add _).curried
val addCurried2 = (n1: Int) => (n2: Int) => n1 + n2
// 无论如何,所有三个调用方式都相同
addMultiple(3)(4) // 7
addCurried1(3)(4) // 7
addCurried2(3)(4) // 7
示例
package org.example.scala
object leftFoldTest extends App{
// 示例1:列表中元素的乘积
val numbers = List(2, 3, 4, 5)
val product = numbers.foldLeft(1)(_ * _)
println("Product of numbers: " + product) // 预期输出: Product of numbers: 120
// 示例2:字符串反转
val str = "Scala"
val reversedStr = str.foldLeft("")((acc, char) => char + acc)
println("Reversed string: " + reversedStr) // 预期输出: Reversed string: alacS
// 示例3:计算列表中偶数的平均值
val values = List(2, 4, 6, 8, 10)
val evenSumAndCount = values.foldLeft((0, 0)) { case ((sum, count), num) =>
if (num % 2 == 0) (sum + num, count + 1) else (sum, count)
}
val evenAverage = evenSumAndCount match {
case (sum, count) if count > 0 => sum.toDouble / count
case _ => 0.0
}
println("Even average: " + evenAverage) // 预期输出: Even average: 6.0
// 示例4:自定义对象列表的操作
case class Person(name: String, age: Int)
val people = List(Person("Alice", 25), Person("Bob", 30), Person("Charlie", 35))
val totalAge = people.foldLeft(0)((sum, person) => sum + person.age)
println("Total age: " + totalAge) // 预期输出: Total age: 90
}
//Product of numbers: 120
//Reversed string: alacS
//Even average: 6.0
//Total age: 90