译者 | TalkingData 李冰心
原文 | https://martinfowler.com/articles/collection-pipeline
译者按:
近来想深入了解下 Java 流底层的实现机制,于是搜索各种资源,最后发现 Martin Fowler 发表的一篇关于集合管道模式的一篇文章,有幸得以拜读,关于集合管道有其独到的见解,看过之后深感佩服,由于原文是英文,索性将其翻译,和大家一起分享,翻译不合理的地方敬请指出。
集合管道
集合管道是一种编程模式,将一些计算转化为一系列操作,通常情况下每个操作的输出结果是一个集合,同时该结果作为下一个操作的输入,常见的操作主要有filter、map和reduce。这种模式常见于函数式编程语言中,因为有了lambdas,这种模式在面向对象语言中也很常见。如果你不熟悉这种模式,通过本文关于构造管道事例的讲解,你会理解其核心概念,从一种语言联想到另外一种语言。
在软件中,集合管道是一种最常见、最令人酣畅淋漓的模式。在 unix 黒与白的命令行中,在面向对象设计语言万物皆对象的类中,在函数式编程语言第一等公民的函数中,都可以发现它们的身影。 不同环境有着不同的表现形式,共通操作有着不同的名称,但是一旦你熟悉了它,你会发现根本离不开它。
初见
第一次接触到这种模式,应该是使用 Unix 的时候。接下来会介绍一些 Unix 的事例,设想下如何在 bliki/entries 目录下查找文件内容包含文本 “nosql” 的文件列表,可以使用grep这样操作:
grep -l ‘nosql’ bliki/entries
每个文件出现单词 “nosql” 的次数:
grep -l ‘nosql’ bliki/entries/* | xargs wc -w
根据次数排序:
grep -l ‘nosql’ bliki/entries/* | xargs wc -w | sort -nr
排序后取top3:
grep -l ‘nosql’ bliki/entries/* | xargs wc -w | sort -nr | head -4 | tail -3
不管之前还是以后,和其他环境的命令行工具相比,这种方式都是无可比拟的。
在开始使用Smalltalk时,发现了相同的模式。 假设有一个文章列表 someArticles,列表中的每篇文章 article 定义了很多标签,同时又统计了单词数量,如果想查找哪些文章包含标签 “nosql”:
someArticles select: [ :each | each tags includes: #nosql]
其中 select 方法使用了Lambda表达式来定义:每篇文章 article 作为输入参数,同时验证标签集合中是否包含 “nosql”。该方法会作用于文章列表的每篇文章,最后输出匹配的结果。
为了排序,扩展了上面的代码:
(someArticles
select: [ :each | each tags includes: #nosql])
sortBy: [:a :b | a words > b words]
sortBy 方法同样使用了Lambda表达式定义:文章互相比较,最终返回一个排序后的文章列表。继续其他操作:
((someArticles
select: [ :each | each tags includes: #nosql])
sortBy: [:a :b | a words > b words])
copyFrom: 1 to: 3
与 unix 管道最核心的相似之处在于:涉及的所有操作例如 select、sortBy 和 copyFrom 输入输出都是集合。unix 中集合是一行行记录构成的流,而Smalltalk集合是对象,但基本概念是相同的。
最近,使用Ruby进行了很多开发,改进后的语法可以更加便捷的操作管道。
some_articles
.select{|a| a.tags.include?(:nosql)}
.sort_by{|a| a.words}
.take(3)
在面向对象语言中,使用方法链构造集合管道是一种很自然的方式。同样也可以使用嵌套函数。
回顾一些基础知识,看看如何使用 lisp 构造类似管道,我会定义存储所有文章的结构体 articles:其中可以通过方法 article-words 和 article-tags 获取内部字段。
基于现有的文章列表 some-articles,获取标签集合中包含 nosql 的文章:
(remove-if-not
(lambda (x) (member 'nosql (article-tags x)))
(some-articles))
为了排序,再次使用了lambda:
(sort
(remove-if-not
(lambda (x) (member 'nosql (article-tags x)))
(some-articles))
(lambda (a b) (> (article-words a) (article-words b))))
使用方法 subseq 获取top3:
(subseq
(sort
(remove-if-not
(lambda (x) (member 'nosql (article-tags x)))
(some-articles))
(lambda (a b) (> (article-words a) (article-words b))))
0 3)
看啊,那就是管道,通过我们一步步的构造,看起来是多么完美。然而最终的表达式是否清晰自然,有待商讨。对于 unix、smalltalk 和 ruby 下管道执行的顺序依赖于内部方法的线性排序。 在你的头脑中应该很容易虚拟化这种场景:左上角的数据经过过滤,最后从右下角输出。 同样可以在 Lisp 中使用嵌套函数,但是你需要掌控函数的嵌套层级。
最近流行的 lisp,Clojure 避免了嵌套层级:
(->> (articles)
(filter #(some #{:nosql} (:tags %)))
(sort-by :words >)
(take 3))
符号 “->>” 是一个线程宏,通过使用 lisp 强大的语法宏功能将每个表达式结果作为下一个表达式的输入。 你可以使用提供的库将嵌套函数转化为线性管道,只需要遵循约定即可。
对于大多数函数开发者来说,可以游刃有余的处理函数的嵌套,线性化无足轻重,所以这就是为什么经过了这么长的时间,lisp才支持操作符 “->>” 。
这些天常听到函粉对于集合管道的赞赏之声,夸奖函数式编程语言的强大,而又感叹面向对象设计语言的不足。这种说法其实非常片面,因为对于众多的 Smalltalk 开发者来说已经广泛的使用了集合管道。而集合管道在面向对象语言中例如 C++、Java 和 C# 中没有被支持的原因,有人说没有借鉴 Smalltalk 的 lambda,此外缺少足够丰富的集合操作。以至于很多面向对象开发人员栽进了集合管道里。 Java 大行其道时不支持lambdas,对于像我这样 Smalltalk 开发者常常会抱怨,但是又不得不忍受。 在 Java 中如何构造集合管道,有过各种尝试,而每种尝试都需要大量的代码来实现,使得即使技术熟练的人也被迫放弃。 在2000年左右我开始使用Ruby,重要原因是因为Ruby支持集合管道。有时候我会感概在我的 Smalltalk 时代,错过了很多有价值的东西。
lambdas 由于短小实用赢得了很多关注,C# 已经支持很多年,现在 Java 也开始支持。 如今你可以在许多语言中使用集合管道。