技术专栏集合管道模式(上)

译者 | 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 也开始支持。 如今你可以在许多语言中使用集合管道。

    原文作者:TalkingData
    原文地址: https://zhuanlan.zhihu.com/p/40604868
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞