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

译者 | TalkingData 李冰心

原文 | https://martinfowler.com/articles/collection-pipeline

前一篇文章中,我们了解了集合管道:集合管道是一种编程模式,将一些计算转化为一系列操作,通常情况下每个操作的输出结果是一个集合,同时该结果作为下一个操作的输入,常见的操作主要有filter、map和reduce。今天我们继续了解集合管道模式的定义等。

二、定义

我认为集合管道是一种指导我们如何模块化和构建软件的模式。和多数模式一样,它经常出现在各种场景中,虽然对此我们习以为常,但是这种模式却别具一格。模式可以解决特定的设计问题,帮助设计者将新的设计建立在以往工作的基础上,复用以往成功的设计方案。

集合管道展示了一系列彼此间传递集合的操作,这些操作的输入输出都是集合,但是其中不包括终端操作,因为终端操作只会输出单个结果。个别的操作可能非常简单,但是你可以使用各种简单操作构造复杂的行为,想象一下现实世界中纵横交错的管道。

集合管道是管道过滤器模式的一个特例,管道过滤器中的过滤相当于集合管道中的操作,之所以没有使用 "过滤" 这个词语,因为 "过滤" 是一种常用的管道操作名称。从另一个角度看,集合管道是一种组成高阶函数的特殊方式,其中涉及的所有函数均作用于某种形式的数据结构,该模式没有确切的名称,需要使用一个新的术语。

操作彼此间传递的信息在不同的环境中有着不同的形式:

  • 在 Unix 中集合是一个由多行文本组成的文件,各种值通过空格连接组成了其中的行,每个值具体表示的含义依赖于行中的排序。管道操作符可以将某个操作的输出重定向到下个操作的输入,集合由管道操作符组成,操作在 Unix 中表示进程。

  • 在面向对象程序中集合用集合类表示,例如 list、array 和 set 等。集合中的每个元素都是对象,对象可以是普通类或集合类的实例。操作是集合类本身(或基类)中定义的各种方法,可以由方法链组成。

  • 在函数式语言中集合与面向对象语言有些类似,集合元素可以定义复杂的层次结构,操作是函数,可以通过嵌套或者使用形成线性表示的运算符组成,例如 Clojure 的箭头运算符。

这种模式也会出现在其它地方。当关系模型首次定义时,其假定所有数据都表示为数学上的关系,就是说n个集合的笛卡儿积的一个子集,数据可以通过关系演算和关系代数的一种方式来操作,你可以将其视作一个集合管道,操作中产生的中间集合被约束为关系。SQL最初作为关系数据库的标准语言而提出,而在实际上总是违背它。所以SQL DBMS实际上不是真正的RDBMS,并且当前ISO SQL标准不提及关系模型或者使用关系术语或概念,SQL使用了一种类似于推导的方式(稍后我会讨论)。

这样一系列转换的概念是软件构建中常见的方法,这也是管道过滤器模式的设计意图。编译器工作原理相似,将源码转换为语法树,途经各种优化,最后输出目标代码。 关于集合管道的区别:各阶段公用的数据结构是集合,最后限定一组特定的公共管道操作。

三、探索更多管道和操作之 map 和 reduce

到目前为止,涉及的是一些常用的管道操作,接下来通过 Ruby 事例代码,让我们来探索更多的操作。诚然使用其它支持该模式的语言,也会构造相同形式的管道。

统计单词总数(map 和 reduce)

技术专栏 | 集合管道模式(下)_第1张图片
Markdown

通过统计所有文章单词总数的例子,让我来介绍下两个最重要的管道操作。

第一个 map:使用给定的 lambda 表达式,作用于输入集合的每个元素,将 lambda 表达式结果以集合的方式返回。

[1, 2, 3].map{|i| i * i} # => [1, 4, 9]

通过使用 map 将文章列表转换为每篇文章单词总数列表。

第二个 reduce:输入集合经过累计运算,最终输出单个结果。具有类似功能的任何函数都可以称作 Reduction,Reduction 在集合管道中总是以终结者的身份最后登场。通常情况下,可以使用两个入参的 lambda 表达式来定义 Ruby 中的 reduce 函数,一个入参是集合元素,一个是累加器。 在 reduce 的过程中,使用 lambda 表达式作用于每个元素,累加器会累计每次 lambda 的返回结果。接下来你可以这样求和:

[1, 2, 3].reduce {|acc, each| acc + each} # => 6

之后使用 map 和 reduce 构造两步操作的管道来统计单词总数。

some_articles
  .map{|a| a.words}
  .reduce {|acc, w| acc + w}

第一步使用 map 将文章列表转换为每篇文章单词数列表,第二步使用 reduce 累计求和。
在这点上,值得一提的是管道上的操作你可以使用不同的方式定义,上面使用的是 lambda,其实仅使用函数名称也是可以的,例如在 Clojure 中:

(->> (articles) 
     (map :words) 
     (reduce +))

该场景中,你只需要关注函数名称,对于 Ruby 也可以使用同样的风格:

some_articles 
    .map(&:words) 
    .reduce(:+)

通常情况下使用函数名称看上去更精炼,但是你会受限于函数的声明和调用方式。lambdas 可以提供更大的灵活性,但是你需要了解更多的语法。关于使用何种语言构造管道,如果 Ruby,我倾向于使用 lambda,如果 Clojure,则是函数名称。具体使用何种方式,你可以自由选择。

四、探索更多管道和操作之 group-by

统计每种类型的文章数(group-by)

技术专栏 | 集合管道模式(下)_第2张图片
Markdown

接下来我们会统计每种类型的文章数,依据统计结果输出的形式,需要使用一个键是类型值是文章数的 hashmap 。
为了解决这个问题,首先我们需要根据类型对所有文章进行分组,这里使用的集合操作就是 group-by,通过使用该操作,会将所有元素射到 hash 中,而索引值依据在此元素上执行给定代码的返回结果。 让我们来看看具体使用的细节:

some_articles
  .group_by {|a| a.type}

然后需要统计每种类型下的文章数。你很可能这样认为,不就是一个简单的 map 操作吗?但实则不然,因为这里需要返回两种维度:分组和数量。这和我们之前介绍 map 的例子有些许联系,但是此时需要使用 group-by 输出 hashmap。
想想开篇中 Unix 的命令行,这个问题在 Unix 中是很常见。集合通常以 list 形式出现,但有时却是 hash,有时候需要在二者之间来回转换。有个取巧的做法是将 hash 视作键值对列表,其中键值对是一种独立结构。关于如何定义 hash 每种语言略有差异,但通常是这样:[key, value]。
Ruby 提供了 to_h 方法可以将数组集合转为 hash:

some_articles
  .group_by {|a| a.type}
  .map {|pair| [pair[0], pair[1].size]}
  .to_h

在管道中 list 和 hash 经常这样互转,但是访问 hash 却需要使用数组下标的方式访问,多少有些怪异,而 Ruby 可以将其解构为两个独立的变量:

some_articles
  .group_by {|a| a.type}
  .map {|key, value| [key, value.size ]}
  .to_h

在函数式编程语言中解构是一种常见的技术,但是传递这些 list-of-hash 数据结构性能上势必会有所损耗。Ruby 的解构语法非常简单,而且足以达到这个简单目的。

同样 Clojure 更是如此:

(->> (articles)
     (group-by :type)
     (map (fn [[k v]] [k (count v)]))
     (into {}))

你可能感兴趣的:(技术专栏 | 集合管道模式(下))