认识不同
Java.net 语言中类似的函数结构
上一期 Java 下一代 文章(“函数式编码风格”)对比和比较了 Scala、Groovy
和 Clojure 中的函数式编码风格。在本文中,系列作家 Neal Ford 将会更深入地剖析 Java
下一代语言中的过滤、映射和缩减(reduce)功能。文中提供的一系列简短的编码示例可帮助您整理这 3 种语言在命名这些重要的函数结构的方式上的比较容易混淆的区别。
函数式编程语言包含多个系列的常见函数。但开发人员有时很难在语言之间进行切换,因为熟悉的函数具有不熟悉的名称。函数式语言倾向于基于函数范例来命名这些常见函数。从脚本背景衍生而来的语言倾向于使用更具描述性的名称(有时是多个名称,包含多个指向同一个函数的别名)。
在本期文章中,我将继续探讨 3 种重要函数(过滤、映射和缩减)的实用性,展示来自每种 Java 下一代语言的实现细节。文中的讨论和示例旨在减轻
3 种语言对类似函数结构使用的不一致名称时可能引起的混淆。
过滤
在过滤 函数中,您可指定一个布尔值条件(通常为一个高阶函数的形式),将它应用到一个集合。该函数返回集合的子集,其中的元素与该条件匹配。过滤与查找
函数紧密相关,后者返回集合中第一个匹配的元素。
Scala
Scala 拥有多个过滤函数变体。最简单的情形基于传递的条件来过滤某个列表。在第一个示例中,我创建一个数字列表。然后使用了
filter() 函数,并传递了一个代码块,指定了所有元素都可以被 3 整除的条件:
val numbers = List.range(1, 11) numbers filter (x => x % 3 == 0) // List(3, 6, 9) |
我可依靠隐式的参数来创建该代码快的更加简洁的版本:
numbers filter (_ % 3 == 0) // List(3, 6, 9) |
第二个版本不那么冗长,因为在 Scala 中,您可以将参数替换为下划线。两个版本都可以得到相同的结果。
过滤操作的许多示例都使用了数字,但 filter() 适用于任何集合。此示例将 filter() 应用到一个单词列表来确定
3 字母单词:
val words = List("the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog") words filter (_.length == 3) // List(the, fox, the, dog) |
Scala 中的另一个过滤函数变体是 partition() 函数,它将一个集合拆分为多个部分。这种拆分基于您传递的高阶函数来确定分离条件。在这里,partition()
函数将返回两个列表,它们依据哪些列表成员可被 3 整除来进行拆分:
numbers partition (_ % 3 == 0) // (List(3, 6, 9),List(1, 2, 4, 5, 7, 8, 10)) |
filter() 函数返回一个匹配元素集合,而 find() 仅返回第一个匹配元素:
numbers find (_ % 3 == 0) // Some(3) |
但是,find() 的返回值不是匹配的值本身,而是一个包装在 Option 类中的值。Option 有两个可能的值:Some
或 None。像其他一些函数式语言一样,Scala 使用 Option 作为一种约定来避免在缺少某个值时返回
null。Some() 实例包装实际的返回值,在 numbers find (_ % 3 == 0) 的情况下,该值为
3。如果我尝试查找某个不存在的值,那么返回值将为 None:
numbers find (_ < 0) // None |
Scala 还包含多个函数,它们基于一个判定函数来处理一个集合并返回值或丢弃它们。takeWhile()
函数返回集合中满足判定函数的最大的值集:
List(1, 2, 3, -4, 5, 6, 7, 8, 9, 10) takeWhile (_ > 0) // List(1, 2, 3) |
dropWhile() 函数跳过满足判定条件的最大元素数量:
words dropWhile (_ startsWith "t") // List(quick, brown, fox, jumped, over, the, lazy, dog) |
Groovy
Groovy 不是一个函数式语言,但它包含许多函数范例,一些范例的名称源自脚本语言。例如,在函数式语言中,该函数在传统上被称为
filter() 的函数,就是 Groovy 中的 findAll() 方法:
(1..10).findAll {it % 3 == 0} // [3, 6, 9] |
像 Scala 的过滤函数一样,Groovy 可处理所有类型,包括字符串:
def words = ["the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"] words.findAll {it.length() == 3} // [The, fox, the, dog] |
Groovy 还有一个类似 partition() 的函数,称为 split():
(1..10).split {it % 3} // [[1, 2, 4, 5, 7, 8, 10], [3, 6, 9]] |
split() 方法的返回值是一个嵌套数组,就像 Scala 中从 partition() 返回的嵌套列表。
Groovy 的 find() 方法返回集合中第一个匹配的元素:
(1..10).find {it % 3 == 0} // 3 |
不同于 Scala,Groovy 遵循 Java 约定,在 find() 未能找到元素时返回 null:
(1..10).find {it < 0} // null |
Groovy 还拥有 takeWhile() 和 dropWhile() 方法,它们具有与 Scala
的版本类似的语义:
[1, 2, 3, -4, 5, 6, 7, 8, 9, 10].takeWhile {it > 0} // [1, 2, 3] |
与 Scala 示例中一样,dropWhile 被用作一个专门的过滤器:它丢弃与判定条件匹配的最大前缀,仅过滤列表的第一部分:
def moreWords = ["the", "two", "ton"] + words moreWords.dropWhile {it.startsWith("t")} // [quick, brown, fox, jumped, over, the, lazy, dog] |
Clojure
Clojure 拥有令人震惊的集合操作例程数量。由于 Clojure 的动态类型,其中许多例程都是通用的。许多开发人员倾向于使用
Clojure,因为它的集合库非常丰富和灵活。Clojure 使用传统的函数式编程名称,如 (filter
) 函数所示:
(def numbers (range 1 11)) (filter (fn [x] (= 0 (rem x 3))) numbers) ; (3 6 9) |
像其他语言一样,Clojure 为简单的匿名函数提供了简洁的语法:
(filter #(zero? (rem % 3)) numbers) ; (3 6 9) |
而且与其他语言中一样,Clojure 的函数适用于任何适用的类型,比如字符串:
(def words ["the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog"]) (filter #(= 3 (count %)) words) ; (the fox the dog) |
Clojure 的 (filter ) 返回类型为 Seq,它通过圆括号来描述。Seq 是 Clojure
中的顺序集合的核心抽象。
映射
所有 Java 下一代语言中常见的第二个主要的函数变形是映射。映射函数接受一个高阶函数和一个集合,然后向每个元素应用传递的函数并返回一个集合。返回的集合(不同于过滤)的大小与原始集合相同,但更新了值。
Scala
Scala 的 map() 函数接受一个代码块并返回转换的集合:
List(1, 2, 3, 4, 5) map (_ + 1) // List(2, 3, 4, 5, 6) |
map() 函数适用于所有适用的类型,但它不一定返回集合元素的已转换集合。在此示例中,我在一个字符串中返回所有元素的大小列表:
words map (_.length) // List(3, 5, 5, 3, 6, 4, 3, 4, 3) |
在函数式编程语言中常常会产生嵌套列表,以至于嵌套列表对解除嵌套(通常称为扁平化)的库支持很常见。以下是扁平化一个嵌套列表的示例:
List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9)) flatMap (_.toList) // List(1, 2, 3, 4, 5, 6, 7, 8, 9) |
获得的 List 中仅包含元素,删除了额外的基础架构。flatMap 函数也适用于可能未以传统方式嵌套的数据结构。例如,您可将一个字符串视为一个嵌套字符系列:
words flatMap (_.toList) // List(t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, ... |
Groovy
Groovy 还包含多个称为 collect() 的映射变体。默认的变体接受一个代码块,以便将该变体应用到集合的每个元素:
(1..5).collect {it += 1} // [2, 3, 4, 5, 6] |
像其他语言一样,Groovy 允许对简单的匿名高阶函数使用简写;it 保留字用于替代单独的参数。
collect() 方法适用于您可向其提供合理的判定条件的任何集合,比如一个字符串列表:
def words = ["the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"] words.collect {it.length()} // [3, 5, 5, 3, 6, 4, 3, 4, 3] |
Groovy 还有一个类似于 flatMap() 的折叠内部结构的方法,称为 flatten():
[[1, 2, 3], [4, 5, 6], [7, 8, 9]].flatten() // [1, 2, 3, 4, 5, 6, 7, 8, 9] |
flatten() 方法也适用于不太明显的集合,比如字符串:
(words.collect {it.toList()}).flatten() // [t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, j, ... |
Clojure
Clojure 包含一个 (map ) 函数,它接受一个高阶函数(其中包含运算符)和一个集合:
(map inc numbers) ; (2 3 4 5 6 7 8 9 10 11) |
(map ) 的第一个参数可以是任何接受单个参数的函数:命名函数、匿名函数或已存在的函数,比如递增其参数的
inc。此示例中演示了更典型的匿名语法,它生成一个字符串中的单词长度的集合:
(map #(count %) words) ; (3 5 5 3 6 4 3 4 3) |
Clojure 的 (flatten ) 函数类似于 Groovy 的:
(flatten [[1 2 3] [4 5 6] [7 8 9]]) ; (1 2 3 4 5 6 7 8 9) |
折叠/缩减
在 3 种 Java 下一代语言中,第三个常见函数在名称上拥有最多变体和许多细微的区别。foldLeft
和 reduce 是一个名为 catamorphism 的列表操作概念上的特定变体,该概念是列表折叠的一种泛化。在此示例中,“折叠左侧”
表示:
使用一个二进制函数或运算符将列表的第一个元素与第二个元素相结合,创建一个新的第一个元素。
单一元素。
请注意,这是您在对一组数字求和时所做的操作:从 0 开始,加第一个元素,将结果与第二个元素相加,一直执行此操作,直到列表元素被用完为止。
Scala
Scala 拥有最丰富的折叠运算集合,这是因为它在一定程度上简化了动态类型的
Groovy 和 Clojure 中没有的多种类型场景。缩减函数常用于执行求和:
List.range(1, 10) reduceLeft((a, b) => a + b) // 45 |
提供给 reduce() 的函数通常是一个接受两个参数,并返回单个结果的函数或运算符,以便可以使用一个列表。您可以使用
Scala 的语法糖来缩短函数定义:
List.range(1, 10).reduceLeft(0)(_ + _) // 45 |
reduceLeft() 函数假设第一个元素是运算的左侧。对于相加等运算符,操作数的位置无关紧要,但放置顺序对相除等运算至关重要。如果希望反转运算符应用的顺序,可以使用
reduceRight():
List.range(1, 10) reduceRight(_ - _) // 5 |
了解何时可使用缩减等高级抽象是掌握函数编程的一个关键。此示例使用 reduceLeft() 来确定集合中最常的单词:
words.reduceLeft((a, b) => if (a.length > b.length) a else b) // jumped |
缩减和折叠运算拥有重叠的功能,它们具有细微的差别,但这不属于本文的讨论范围。但是,通常可以看到它们的一个明显区别。在
Scala 中,签名 reduceLeft[B >:A](op:(B, A) => B):B
表明惟一想要的参数就是组合元素的函数。初始值应该是集合中的第一个值。相对而言,签名 foldLeft[B](z:B)(op:(B,
A) => B):B 表示结果的一个初始种子值,所以您可以返回与列表元素类型不同的类型。
以下是一个使用 foldLeft 对集合求和的示例:
List.range(1, 10).foldLeft(0)(_ + _) // 45 |
Scala 支持运算符重叠,所以两个常见的折叠操作 foldLeft 和 foldRight 分别拥有相应的运算符:/:
和 :\。因此,您可以使用 foldLeft 创建 sum 的简洁版本:
(0 /: List.range(1, 10)) (_ + _) // 45 |
类似地,要找到一个列表中每个元素的级联区别(求和运算的反向操作,无可否认这种需求很少见),您可以使用
foldRight() 函数或 :\ 运算符:
(List.range(1, 10) :\ 0) (_ - _) // 5 |
Groovy
Groovy 通过使用重叠来支持与 Scala 的 reduce() 和
foldLeft() 选项相同的功能,从而进入缩减类别。该函数的一个版本接受一个初始值。此示例使用 inject()
方法生成一个集合的总和:
(1..10).inject {a, b -> a + b} // 55 |
替代形式接受一个初始值:
(1..10).inject(0, {a, b -> a + b}) // 55 |
Groovy 拥有一个比 Scala 或 Clojure 小得多的函数库 — Groovy 是一种不强调函数式编程的多范例编程,看到这种情况毫不奇怪。
Clojure
Clojure 主要是一种函数式编程语言,所以它支持 (reduce )。(reduce
) 函数接受一个可选的初始值,以便同时涵盖 Scala 所处理的 reduce() 和 foldLeft()
情形。(reduce ) 函数没有给用户带来任何惊喜。它接受一个需要两个参数的函数和一个集合:
(reduce + (range 1 11)) ; 55 |
Clojure 将对类似 reduce 的功能的高级支持包含在一个名为 reducers 的库中,后面的一期文章将会介绍这个库。
结束语
学习不同范例(比如函数式编程)的部分挑战在于学习新术语。在不同社区使用不同的词汇时,这一过程会变得更加复杂。但一旦掌握了相似性,您就会看到,所有
3 种 Java 下一代语言都以令人惊奇的方式在语法上提供了重叠的功能。
在下一期中,我将探讨 Java 下一代语言中的内存化,讨论函数特性如何结合使用来实现简洁有力。
|