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

译者 | 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)

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)

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 {}))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容