水管工、锁链与著名画家:R 语言管道操作符的历史

Hacker News Top 新闻

摘要

博客文章回顾了 R 语言中管道操作符(%>% / |>)的历史,从 2012 年 StackOverflow 上的提问、Ben Bolker 的首个实现,到 dplyr 中 chain 与 %.% 操作符的引入与发展。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/10/11 15:40

# 管工、链子与著名画家:R 中管道操作符的(更新版)历史 | Adolfo Álvarez 来源:http://adolfoalvarez.cl/blog/2021-09-16-plumbers-chains-and-famous-painters-the-history-of-the-pipe-operator-in-r/ **注意:**本文是我于2014年在本站旧版本 (https://web.archive.org/web/20150225020645/http://adolfoalvarez.cl/plumbers-chains-and-famous-painters-the-history-of-the-pipe-operator-in-r/) 上发表的另一篇文章的更新版。 我们的故事始于许多 R 相关故事的起点……Stack Overflow (http://stackoverflow.com/)。2012年1月17日 (http://stackoverflow.com/questions/8896820/how-to-implement-fs-forward-pipe-operator-in-r),一位匿名用户"user4"提出了以下问题: > 如何在 R 中实现 F# 的前向管道操作符? 该操作符可以轻松地将一系列计算串联起来。例如,当你有一份输入数据并想依次调用 `foo` 和 `bar` 函数时,可以这样写: > `data |> foo |> bar` Ben Bolker (https://twitter.com/bolkerb) 在当天回答了这个问题 (http://stackoverflow.com/a/8897778),给出了可以被认为是 R 中第一个管道的实现: ``` "%>%" <- function(x,f) do.call(f,list(x)) pi %>% sin %>% cos ``` ``` ## [1] 1 ``` 虽然可能第一次在 R 包中正式引入管道函数发生在2012年4月5日 (https://github.com/crowding/vadr/commit/9509884b1e88e1310d964acab7c6d9838df9a639#diff-d725d41bc2e847af038b2b15e28d8520831f4f69fcc0f87a6afe97bbb5df9c68),当时 Peter Meilstrup (https://twitter.com/perceptophore) 创建了 `ptools` 包,用于收集"各种数据操作和编程工具"。其中一个工具就是 `chain` 函数,实现了管道传递参数的方式。该包后来更名为 `vadr` (https://github.com/crowding/vadr),`chain` 函数至今仍可在其中找到。 ## dplyr 但很快,一个为提高管道使用流行度做出重要贡献的包即将诞生:2012年10月28日 (https://github.com/tidyverse/dplyr/commit/80dc69b144711ec095db1d62cf0b73e09560eaf0),Hadley Wickham (https://twitter.com/hadleywickham) 在 GitHub 上启动了 `dplyr` 项目 (https://github.com/tidyverse/dplyr/),作为其数据分析包 `plyr` (https://github.com/hadley/plyr) 的演进版本(最初该包确实被命名为 'plyr2')。 最初,dplyr 包并没有包含链式函数,但大约一年后的2013年10月9日 (https://github.com/tidyverse/dplyr/commit/57f76ed560ac52590f7ded3d013db8fb19f7d627),dplyr 中出现了第一个管道。该函数同样被命名为 `chain`,但包中还引入了第一个管道操作符:`%.%`。 引入链式函数的目的是简化对数据框应用多个函数的写法。在没有 `chain` 函数时,在 dplyr 中需要从内向外阅读各个动词: ``` library(hflights) library(dplyr) filter( summarise( select( group_by(hflights, Year, Month, DayofMonth), Year:DayofMonth, ArrDelay, DepDelay), arr = mean(ArrDelay, na.rm = TRUE), dep = mean(DepDelay, na.rm = TRUE) ), arr > 30 | dep > 30 ) ``` ``` ## # A tibble: 14 × 5 ## # Groups: Year, Month [10] ## Year Month DayofMonth arr dep ## ## 1 2011 2 4 44.1 47.2 ## 2 2011 3 3 35.1 38.2 ## 3 2011 3 14 46.6 36.1 ## 4 2011 4 4 38.7 27.9 ## 5 2011 4 25 37.8 22.3 ## 6 2011 5 12 69.5 64.5 ## 7 2011 5 20 37.0 26.6 ## 8 2011 6 22 65.5 62.3 ## 9 2011 7 29 29.6 31.9 ## 10 2011 9 29 39.2 32.5 ## 11 2011 10 9 61.9 59.5 ## 12 2011 11 15 43.7 39.2 ## 13 2011 12 29 26.3 30.8 ## 14 2011 12 31 46.5 54.2 ``` 但当 `chain` 函数被引入后,代码首先变成了这样: ``` chain( hflights, group_by(Year, Month, DayofMonth), select(Year:DayofMonth, ArrDelay, DepDelay), summarise( arr = mean(ArrDelay, na.rm = TRUE), dep = mean(DepDelay, na.rm = TRUE) ), filter(arr > 30 | dep > 30) ) ``` 使用 `%.%` 管道后则变为: ``` hflights %.% group_by(Year, Month, DayofMonth) %.% select(Year:DayofMonth, ArrDelay, DepDelay) %.% summarise( arr = mean(ArrDelay, na.rm = TRUE), dep = mean(DepDelay, na.rm = TRUE) ) %.% filter(arr > 30 | dep > 30) ``` ## magrittr 然而,`%.%` 管道在 `dplyr` 包中并没有存在太久。2013年12月29日 (http://stackoverflow.com/revisions/20828881/1),Stefan Bache (http://www.stefanbache.dk/) 回顾了旧的 Stack Overflow 问题,提出了原始答案的替代方案: > 试试这个如何? > `` `%>%` <- function(e1, e2) { > cl <- match.call() > e <- do.call(substitute, list(cl[[3]], list(. = cl[[2]]))) > eval(e) > } `` > 它允许这样的链式调用: > `` iris %>% subset(., Species == "setosa", select = -Species) %>% colMeans(.) `` > ``` > ## Sepal.Length Sepal.Width Petal.Length Petal.Width > ## 5.006 3.428 1.462 0.246 > ``` Stefan 继续开发这个管道操作,2013年12月30日 (http://stackoverflow.com/revisions/20828881/4),他在 GitHub 上实现了包含 `%>%` 操作符的 `plumbr` 包。两天后 (http://stackoverflow.com/revisions/20828881/5),`plumbr` 被更名为 `magrittr` (https://github.com/smbache/magrittr),即其现名,这明显致敬了比利时画家 René Magritte 的名画"形象的背叛"(The Treachery of Images)(http://en.wikipedia.org/wiki/The_Treachery_of_Images)。 `dplyr` 包在并行开发,但这两个项目本意就是要汇合。2014年1月,RStudio 博客发布了 `dplyr` 的介绍文章 (http://blog.rstudio.org/2014/01/17/introducing-dplyr/),Stefan Bache 在第一条评论中向 Hadley Wickham 介绍了 `magrittr` 操作符。之后,2014年3月19日 (https://github.com/tidyverse/dplyr/commit/3c91ba370286025c31f624317ebce421d1c70caa),`chain` 函数在 dplyr 中被标记为废弃,最终在2014年4月14日 (https://github.com/tidyverse/dplyr/commit/89aaa9a8b386d7e3705ffb682d89dc5bebe823c0),`dplyr` 纳入了 magrittr 的 `%>%` 操作符,并推荐用它替代原来的 `%.%`,因为前者只需按住 Shift 键就更容易输入。 2014年8月1日 (https://github.com/tidyverse/dplyr/commit/67183e2771f24f59330fdaae1bb58b627a58a8ff),`%.%` 被标记为废弃。 与此同时,2014年7月31日 (https://github.com/rstudio/rstudio/commit/861d4e36ffd0233ee41bfb80c712a635c70c1b3c),RStudio IDE v0.98.987 版本为 `dplyr`/`magrittr` 管道操作符 `%>%` 加入了快捷键,使使用更加便捷(Shift+Alt+.),但很快便更改为 (https://github.com/rstudio/rstudio/commit/0fb46fbc114a91217fae106bac42e82f60a02477) Ctrl+Shift+M,即其当前形式。 故事到这里,我推荐阅读2014年8月5日的一篇博客文章,Stefan 在其中讲述了他是如何开始 magrittr 项目的 (https://www.r-statistics.com/2014/08/simpler-r-coding-with-pipes-the-present-and-future-of-the-magrittr-package/)。 ## pipeR R 中管道实现的另一个迭代始于2014年4月7日,Kun Ren (https://twitter.com/renkun_ken) 在 GitHub 上发布了 `pipeR` 包 (https://github.com/renkun-ken/pipeR/),引入了不同的管道操作符 `%>>%`,为管道过程增加了更多灵活性。该包包含了多种管道特性,例如作为函数的第一个参数、作为表达式中的参数(使用 `.`)、使用公式、保存中间结果(使用 `~`)、或按名称提取元素(使用 `()`): ``` library(pipeR) mtcars %>>% (lm(mpg ~ wt + cyl, data = .)) %>>% (~ lm_mtcars) %>>% summary %>>% (r.squared) ``` ``` ## [1] 0.8302274 ``` 目前 pipeR 自2016年以来没有更新,但仍然可以在 CRAN 和 GitHub 上获取,如果你想尝试的话。 ## wrapr 2018年7月 (https://github.com/WinVector/wrapr/commit/6bb774f090c6aec457c472703c4759e98cc25e38),`wrapr` 包 (https://winvector.github.io/wrapr/) 引入了"点箭头管道"(dot arrow pipe)(https://winvector.github.io/wrapr/reference/dot_arrow.html) `%.>%`,其中记法 `a %.>% b` 几乎等价于 `. <- a; b`。例如: ``` library(wrapr) 1:10 %.>% mean(.) ``` ``` ## [1] 5.5 ``` 而左值仍然存储在环境中: ``` ## [1] 1 2 3 4 5 6 7 8 9 10 ``` 更多细节请见这里 (https://journal.r-project.org/archive/2018/RJ-2018-042/RJ-2018-042.pdf) ## Base R R 中的管道彻底改变了我们在 R 中编写代码的方式,使代码更加简洁易读。因此,将管道纳入 base R 只是时间问题。 最早的尝试之一由 Lionel Henry (https://twitter.com/_lionelhenry) 开发,他于2016年2月开始在 base R 中测试原生管道 (https://github.com/lionel-/r-source/tree/pipe)。他写了一篇非常值得推荐的文章 (https://lionel-.github.io/2016/02/15/ideas-for-an-updated-r-syntax/),讨论了这一提案以及 R 核心团队的其他提议,设想了一个拥有原生管道操作符的 R 4.0。 实现这一功能的原因之一是"管道的原生处理将改善错误恢复",因为它消除了对外部包(`magrittr`)的依赖。 到这里,我们可能错过了故事的一部分。似乎在此后的三年中这一主题没有太大进展,尽管 R Core 团队内部可能进行了讨论。 2019年10月5日,Antoine F. 在 r-devel 邮件列表中提问:base R 应该有管道操作符吗?(https://stat.ethz.ch/pipermail/r-devel/2019-October/078520.html) 这个问题确实引发了多位 R 开发者的回复 (https://stat.ethz.ch/pipermail/r-devel/2019-October/thread.html#78520),其中包括 Lionel Henry、Duncan Murdoch 和 Luke Tierney 等人。 实际上,在2020年7月8日,Luke Tierney (https://stat.uiowa.edu/people/luke-tierney) 在他与 Martin Mächler 于 UseR! 2020 (https://user2020.r-project.org/) 上的主题演讲中谈到了 R 的新发展。演讲的一部分 (https://www.youtube.com/watch?v=X_eDHNVceCU&t=4082s) 专门讨论了管道,他预告说,得益于 Lionel Henry 和 Jim Hester 的想法,一个 base 管道 `|>` 正在开发中。 最终,在2020年底,`|>` 诞生了。这个原生管道在 R 的开发版本中被正式宣布 (https://developer.r-project.org/blosxom.cgi/R-devel/NEWS/2020/12/04),随即在 r-devel 邮件列表 (https://stat.ethz.ch/pipermail/r-devel/2020-December/thread.html#80173) 和整个 R 社区中引发了即时反响(例如这里 (https://win-vector.com/2020/12/07/my-opinion-on-rs-upcoming-pipe/)、这里 (https://news.ycombinator.com/item?id=25316608)、这里 (https://www.jumpingrivers.com/blog/new-features-r410-pipe-anonymous-functions/) 和这里 (https://www.infoworld.com/article/3621369/use-the-new-r-pipe-built-into-r-41.html))。 管道最终在 R 稳定版本 4.1.0 中落地,该版本于2021年5月18日发布并正式宣布 (https://stat.ethz.ch/pipermail/r-announce/2021/000670.html)。 现在,我们最初从 dplyr 文档中取出的第一个示例可以这样书写: ``` hflights |> group_by(Year, Month, DayofMonth) |> select(Year:DayofMonth, ArrDelay, DepDelay) |> summarise( arr = mean(ArrDelay, na.rm = TRUE), dep = mean(DepDelay, na.rm = TRUE) ) |> filter(arr > 30 | dep > 30) ``` ``` ## # A tibble: 14 × 5 ## # Groups: Year, Month [10] ## Year Month DayofMonth arr dep ## ## 1 2011 2 4 44.1 47.2 ## 2 2011 3 3 35.1 38.2 ## 3 2011 3 14 46.6 36.1 ## 4 2011 4 4 38.7 27.9 ## 5 2011 4 25 37.8 22.3 ## 6 2011 5 12 69.5 64.5 ## 7 2011 5 20 37.0 26.6 ## 8 2011 6 22 65.5 62.3 ## 9 2011 7 29 29.6 31.9 ## 10 2011 9 29 39.2 32.5 ## 11 2011 10 9 61.9 59.5 ## 12 2011 11 15 43.7 39.2 ## 13 2011 12 29 26.3 30.8 ## 14 2011 12 31 46.5 54.2 ``` 与此同时,`magrittr` 包也在持续演进,并逐步向原生管道靠拢。2016年转入 tidyverse 系列包之后,2020年11月发布了2.0版本 (https://www.tidyverse.org/blog/2020/11/magrittr-2-0-is-here/)。 正如公告文章中提到的,`magrittr` 的这个新版本用 C 语言重写了 `%>%` 管道,实现了显著的速度提升。一篇包含类似结果的更深入的基准测试分析也可以在这里找到 (https://michaelbarrowman.co.uk/post/the-new-base-pipe/)。 正如 Jumping Rivers 关于 R 4.1 发布的这篇博文 (https://www.jumpingrivers.com/blog/new-features-r410-pipe-anonymous-functions/) 所指出的,`magrittr` 管道与原生管道之间存在一些差异。例如,使用 `magrittr` 时不需要在函数名末尾写括号,而在原生管道中这是必须的: ``` library(magrittr) 1:10 %>% mean() ``` ``` ## [1] 5.5 ``` ``` ## [1] 5.5 ``` ``` ## [1] 5.5 ``` ``` ## Error: The pipe operator requires a function call as RHS ``` 另一个可能更重要的区别是点(`.`)替换。默认情况下,管道将 lhs(左侧值)作为 rhs(右侧)的第一个参数传递,正如上面的例子中,1:10 被用作 mean() 的第一个参数。使用 `magrittr` 管道时,lhs 可以作为 rhs 的任何其他参数使用,只需在 lhs 应放置的位置写一个点(`.`)。例如,以下代码在 magrittr 中可以工作[^1]: ``` library(palmerpenguins) library(rpart) library(rpart.plot) penguins %>% rpart(species ~ ., method = "class", data = .) %>% prp(extra = 4) ``` 但以下代码不行: ``` penguins |> rpart(species ~ ., method = "class", data = .) |> prp(extra = 4) ``` ``` ## Error in model.frame.default(formula = penguins, data = ., weights = species ~ : ## 'data' must be a data.frame, environment, or list ``` 要使它正常工作,我们需要使用 R 4.1.0 的另一个新特性——简写内联函数记法 `\(x)`: ``` penguins |> {\(x) rpart(species ~ ., method = "class", data = x)}() |> prp(extra = 4) ``` 虽然有人曾提议在 R base 中引入新的管道 (https://stat.ethz.ch/pipermail/r-devel/2021-January/080396.html) `=>` 来将 lhs 传递给其他参数(另见这里 (https://twitter.com/rdataberlin/status/1396144404033949700)),但它仍未被正式支持 (https://stat.ethz.ch/pipermail/r-devel/2021-April/080645.html)。之前的选项 (https://www.youtube.com/watch?v=X_eDHNVceCU&t=3099s) 还包括使用 `_` 作为 `.` 占位符的替代。我们需要等待看看第二种管道记法是否会被采纳,还是他们选择保留 `|>` 管道和 `\(x)` 解决方案。 9年之后,作为众多 R 用户和开发者努力与时间的成果,我们的 "user4" 终于在 R 中完全实现了她期望的 `data |> foo |> bar`,改变了许多人的编码方式。我很想知道第5版、第6版或第7版会带给我们什么,感谢阅读! - `magrittr` 包还实现了另外三种管道 (https://data-and-the-world.onrender.com/posts/magrittr-pipes/)(`%T>%`、`%$%` 和 `%<>%`) - `data.table` 包不包含通用管道,但你可以用 `]\` 管道传递函数 (https://www.gl-li.com/2017/07/25/compare-data.table-pipes-and-magrittr-pipes/) - `magrittr` 管道和 base R 管道之间的另一个区别可能是发音:虽然 Hadley Wickham 说过 (https://twitter.com/hadleywickham/status/507643720401842176) `%>%` 管道应读作"然后"(then),但我们可以清楚地听到 Luke Tierney 在他的 UseR! 主题演讲中……

相似文章

管道、Fork 和僵尸进程

Hacker News Top

本文来自哈佛大学的 CS 61 课程,涵盖了 Unix 中的管道、Fork 和僵尸进程概念,解释了在关闭时管道如何自动终止程序,以及如何使用管道实现对子进程的阻塞等待。

实验 – Projectional Viewer

Hacker News Top

这个实验利用OhmJS和RWR的管道在Emacs中隐藏代码实现细节,通过专注于结构来提高代码可读性。

能坦承"我不知道"的生产级 RAG 流水线

Reddit r/ArtificialInteligence

介绍如何用 Postgres + pgvector、FastAPI、Gemini、Celery 和 Redis 构建生产级 RAG 流水线:答案引用来源、在不知道时拒绝回答,并在数据库层面强制执行行级安全。