集合管道 Collection Pipeline【译】

633 阅读6分钟

原文:martinfowler.com/articles/co…

作者:Martin Fowler

初遇

学习Unix的时候,我第一次接触到了集合管道。例如:

1. 我们想要在bliki/entries文件夹中查找所有包含“nosql”的文件,可以使用grep命令

grep -l 'nosql' bliki/entries/*

2. 我想知道这些匹配的文件中有多少单词

grep -l 'nosql' bliki/entries/* | xargs wc -w 

3. 按照单词的数量排序

grep -l 'nosql' bliki/entries/* | xargs wc -w | sort -nr

4. 打印出前3位(移除total)

grep -l 'nosql' bliki/entries/* | xargs wc -w | sort -nr | head -4 | tail -3

和我之前(包括之后)使用的命令行环境相比,这极其强大。

随后不久,我在开始使用Smalltalk时,发现了相同的模式。假设我们有一个article对象的集合,每个对象(或者说其中一些对象)存在一个tag集合和一个单词数,我能通过下面的语句,挑选出存在#nosql tag 的article:

someArticles select: [ :each | each tags includes: #nosql]

这个选择方法使用了一个单独的参数Lambda(方括号定义的,在Smalltalk中被称为“block”),定义了一个布尔函数,作用于所有的article,并且返回lambda 结果为true的article集合。 为了给结果排序,我扩展了代码如下:

(someArticles
      select: [ :each | each tags includes: #nosql]) 
      sortBy: [:a :b | a words > b words]

这个排序方法同样拥有一个lambda参数,用来给元素排序,和select一样,它返回一个新的集合,流向我们下一个管道

((someArticles 
      select: [ :each | each tags includes: #nosql])
      sortBy: [:a :b | a words > b words]) 
      copyFrom: 1 to: 3

与Unix管道核心的相似之处在于,这些操作方法(select, sortBy, copyFrom)操作一个有操作记录的集合并且返回一个有操作记录的集合。在Unix中,这个集合是一个流,其中操作记录作为流中的line,在Smalltalk中,集合是对象,但基本的概念是相同的。

近日,我在Ruby中做了很多编程,在Ruby语法中,创建一个集合管道变得更加容易了,我不需要用括号包裹之前的管道状态。

some_articles
  .select{|a| a.tags.include?(:nosql)}
  .sort_by{|a| a.words}
  .take(3)

在面向对象编程中,创建一个集合管道作为一个方法链是一个很自然的做法。

但是,可以通过嵌套函数调用来实现相同的想法。

回到基础知识,我们来探讨一下如何在通用lisp中设置相似的管道,我们可以将每个article存储在名为articles的结构中,这使我可以通过函数访问类似article-words and和article-tags的字段,some-articles函数返回我开始的初始值。

第一步:选择存在nosql的articles

(remove-if-not
   (lambda (x) (member 'nosql (article-tags x)))
   (some-articles))

仿照Smalltalk 和Ruby,我使用了remove-if-not函数,该函数同时操作列表和用lambda来定义断言,所以我能够扩展表达式来给他们排序,再次使用lambda

第二步:排序

(sort
   (remove-if-not
      (lambda (x) (member 'nosql (article-tags x)))
      (some-articles))
   (lambda (a b) (> (article-words a) (article-words b))))

第三步:用subseq方法挑选出前三位

(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强大的语法宏功能将每个表达式的结果线程化为下一个表达式的参数。让你能够遵循库中的约定(例如,使主体集合成为每个转换函数中的最后一个参数),你可以使用它来将一系列嵌套函数转换为线性管道。

但是,对于许多函数式程序员而言,使用这种线性方法并不重要。这些程序员能够很好的处理嵌套函数的深度排序,这就是为什么像“->>”这样的运算符这么长时间才能流行。

这些天,我经常听到函数式编程的爱好者赞美集合管道的优点,说他们有面向对象所缺少的强大的功能特性。作为老的Smalltalker,我觉得这很烦人,因为Smalltalkers广泛使用它们。人们说集合管道不是OO(面向对象编程,下文简写为OO)特性的主要原因是,流行的OO语言,例如C++,java,C#没有采用像Smalltalk中对lambda的使用,因此没有丰富的数组集合操作方法来支持集合管道模式。所以,对于大多数面向对象的程序员来说,集合管道已经消失了。当Java成为大佬时,像我这样的Smalltalkers 们抱怨缺少lambda,但是,我们不得不忍受它。

在java中,我们做过多种多样的尝试来使用集合管道,毕竟,对OOer来说,函数就是一个只有一个方法的类。但最终的代码实在太混乱了,导致对这些技术很熟悉的开发者也趋向于放弃。

Ruby对集合管道的舒适支持是我在2000年左右开始大量使用Ruby的主要原因之一。我在使用Smalltalk 的日子里,错过了很多类似的功能。

如今,lambda作为一个高级且小众的语言特性,褪去了很多荣誉,在主流语言中,C#已经加入几年了,甚至Java也终于加入了,因此,集合管道在很多语言中都是可行的。

定义集合管道

...

探索更多的管道和操作

...

备选方案

...

嵌套运算符表达式

对集合进行一系列操作是你能够用集合做的有效操作之一。我们假设我正在通过函数搜索一个酒店中的房间,这个函数能够返回各种类型的房间,【红色】,【蓝色】,【在就酒店前面】或者【正在使用的】房间,我能够通过一个表达式找【出在酒店前面】【空闲的】【红色或蓝色房间】。

  • Ruby
(front & (red | blue)) - occupied
  • Clojure…
(difference
 (intersection
  (union reds blues)
  fronts)
 occ)

Clojure 在它的 datatype定义了一系列操作, 这里的所有符号是已定义的

我可以将这些表达式格式化为集合管道

  • Ruby
red
  .union(blue)
  .intersect(front)
  .diff(occupied)

  • Clojure…
(->> reds
     (union blues)
     (intersection fronts)
     (remove occ))

使用remove方法来获取正确的参数顺序给线程