比较R语言中的管道操作符:|> vs. %>%

Yao Zhang

May 31, 2023

R语言4.1.0版本引入了原生的管道操作符|>,它的工作原理与magrittr包中提供的管道操作符%>%类似。例如,在下面这个例子中,LHS |> RHSLHS %>% RHS这两种写法,均可以将LHS的输出结果作为RHS第一个参数的输入值,从而实现流程化的管道操作效果。

library(magrittr)
library(tidyverse)

## 使用R语言原生的管道操作符 |> 
diamonds |> 
        group_by(cut) |> 
        summarise(across(.col = c("carat", "price"),
                         .fns = sum,
                         .names = "sum_of_{.col}")) |> 
        mutate(price_per_carat = sum_of_price/sum_of_carat)
# A tibble: 5 × 4
  cut       sum_of_carat sum_of_price price_per_carat
  <ord>            <dbl>        <int>           <dbl>
1 Fair             1684.      7017600           4167.
2 Good             4166.     19275009           4627.
3 Very Good        9743.     48107623           4938.
4 Premium         12301.     63221498           5140.
5 Ideal           15147.     74513487           4919.
## 使用magrittr包提供的管道操作符 %>% 
diamonds %>% 
        group_by(cut) %>% 
        summarise(across(.col = c("carat", "price"),
                         .fns = sum,
                         .names = "sum_of_{.col}")) %>% 
        mutate(price_per_carat = sum_of_price/sum_of_carat)
# A tibble: 5 × 4
  cut       sum_of_carat sum_of_price price_per_carat
  <ord>            <dbl>        <int>           <dbl>
1 Fair             1684.      7017600           4167.
2 Good             4166.     19275009           4627.
3 Very Good        9743.     48107623           4938.
4 Premium         12301.     63221498           5140.
5 Ideal           15147.     74513487           4919.

虽然从结果上看,二者并没有什么区别,但原生的管道操作符|>在实际使用时有些需要注意的地方。为了说明这点,需要先回顾一下%>%的一些特性。

一、%>%的一些特性

1. %>%是一个function

R语言中创建function需要用到function()函数,大多数时候我们都会将这个函数的结果赋值给一个变量,例如my_func <- function(x) { ... }这行代码创建了一个名为my_func的function,使用时则需要调用my_func(x)来复现{ ... }中的逻辑。

实际上R语言还有另外一种创建function的方式,作为使用者虽然用的不多,但作为开发者则会经常用到,那就是类似+(加)、-(减)、*(乘)、/(除)这样的function。这种function最大的特点就在于调用时不需要加(),且参数是写在function的左右两端,例如下面这个例子:

## 创建一个名为%A_and_B%的function
`%A_and_B%` <- function(A, B) { paste(A, B, sep = " and ") }

## 调用%A_and_B%
"矿爹" %A_and_B% "杨姐"
[1] "矿爹 and 杨姐"

可以看到,我们创建了一个名为%A_and_B%的function,这个function的功能是将参数AB通过and连接起来,但是其调用方式与传统的my_func(x)不同,我们直接将AB分别写在这个function的左右两边("矿爹" %A_and_ _B% "杨姐")即可实现调用。

与这个例子类似的,我们可以看出管道操作符%>%实际上是magrittr包提供的一个function,它与一般的function除了在调用方式上不同以外,并没有什么本质区别。

2. 使用%>%时如果不想传参给右侧第一个输入怎么办?

在开头部分我们提到:

LHS |> RHSLHS %>% RHS这两种写法,均可以将LHS的输出结果作为RHS第一个参数的输入值,从而实现流程化的管道操作效果。

那如果不想把左侧的结果传给右侧的第一个参数,而是传给右侧某个指定的参数,要如何实现呢?

答案是:使用占位符.。我们来看下面这个例子:

head(iris) # 鸢尾花数据
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa
iris %>% 
        plot(Sepal.Width ~ Sepal.Length, col = Species) # 这样作图会报错
Error in pairs.default(data.matrix(x), ...): object 'Species' not found

上面作图的代码之所以会报错,是因为plot()函数中的第一个参数不是data,而我们希望把iris数据传给data,此时便可以使用占位符.来实现我们想要的效果:

iris %>% 
        plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)

由于占位符.可以代替左侧的输出结果,那我们自然会想到,如果将上面的代码换一种写法,是不是也能成功呢?

iris %>% 
        plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species)
Error in plot.xy(xy, type, ...): invalid plot type

很遗憾,这样的写法是行不通的……正确的做法需要用{}plot()函数包起来。

iris %>% 
       { plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species) }

这样得到的图形结果,与第一种写法相比,除了X轴和Y轴的标题不同,其余部分都是一样的。

当然,使用with()函数配合占位符.也是能得到同样的结果的,某种程度上可能更易于理解。

iris %>% 
        with(., plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species))

总之,占位符.作为magrittr包提供的一个功能,在很多场合都非常实用,例如我们希望将diamonds数据当中重量大于等于1克拉的钻石价格存在一个名为diamonds_price_1vector里,如果用管道符%>%来进行操作,大致应该是这样:

diamonds_price_1 <- diamonds %>% 
        filter(carat >= 1) %>% 
        select(price)

head(diamonds_price_1)
# A tibble: 6 × 1
  price
  <int>
1  2774
2  2781
3  2788
4  2788
5  2789
6  2789

这样做本身并没有任何问题,但如果我们检查一下diamonds_price_1的类型会发现,它并不是一个vector,而是一个只有一列的data.frame

class(diamonds_price_1)
[1] "tbl_df"     "tbl"        "data.frame"
dim(diamonds_price_1)
[1] 19060     1

假如因为某些原因,我们必须将结果存成vector,此时我们当然可以选择再进行一步操作,将它转为一个vector并保存:

diamonds_price_1.vec <- diamonds_price_1$price

class(diamonds_price_1.vec)
[1] "integer"
head(diamonds_price_1.vec)
[1] 2774 2781 2788 2788 2789 2789

但这样的操作打断了管道符操作的连贯性,也失去了使用管道符操作的初衷。

所以我们可以使用占位符.来保证管道操作的连贯性。

diamonds_price_1 <- diamonds %>% 
        filter(carat >= 1) %>% 
        select(price) %>% # 这一步并不是必须的,仅为对比演示需要而保留
        .$price

class(diamonds_price_1)
[1] "integer"
head(diamonds_price_1)
[1] 2774 2781 2788 2788 2789 2789

可以看到结果变成了vector。这是因为在占位符.之前输出的结果是一个data.frame,我们用占位符.代替了这个data.frame,再使用$选取price这一列,这样得到的结果就是vector了。

又或者,我们可以直接使用pull()函数,得到的结果也是一个vector

diamonds_price_1 <- diamonds %>% 
        filter(carat >= 1) %>% 
        pull(price)

class(diamonds_price_1)
[1] "integer"
head(diamonds_price_1)
[1] 2774 2781 2788 2788 2789 2789

二、|>%>%的区别

本文开头的例子已经向大家展示了,在基本功能上|>%>%并没有任何区别,都是将LHS的输出结果传参给RHS的第一个输入。他们最大的不同可能就在于占位符.magrittr包提供的,因此当使用R原生的管道操作符|>时,是无法使用占位符.的。

虽然只有这一点点的区别,但在实际使用习惯了占位符.的情况下,还是会带来一些不便,例如将之前的作图代码中的%>%替换为|>后就会报错。

iris |> 
        plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
Error in pairs.default(data.matrix(x), ...): object 'Species' not found

或者

iris |> 
        { plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species) }
Error: function '{' not supported in RHS call of a pipe

又或者

iris |> 
        with(., plot(x = .$Sepal.Length, y = .$Sepal.Width, col = .$Species))
Error in eval(substitute(expr), data, enclos = parent.frame()): object '.' not found

如何绕开这一点限制呢?这里涉及到一个很重要的知识点,那就是匿名function。

三、匿名function

匿名function,顾名思义就是没有名字的function,与一开始提到的my_func的例子不同,匿名function不会将function()函数定义好的逻辑赋值给一个变量(因此匿名)。它随写随用,用完即弃,无法像my_func那样被复用(在Python中,这种function也被称为lambda function)。

x <- 0:9
sapply(x, function(x) { x + 1 })
 [1]  1  2  3  4  5  6  7  8  9 10

在上面这个例子当中,我们定义了一个匿名function:function(x) { x + 1 },它没有名字,其作用是给它一个x,它会给这个x加1。所以当我们给它从0到9这十个数字时,它返回了1到10。

R语言中function还有一种简便写法,例如

function(x) { x + 1 }

可以简化为

\(x) { x + 1 }

也就是用\替代function。所以刚才的例子可以简化的写为:

x <- 0:9
sapply(x, \(x) { x + 1 })
 [1]  1  2  3  4  5  6  7  8  9 10

注意,这种写法不局限于匿名function,非匿名function也可以这样写,但既然要创建非匿名function,还是尽量避免这种写法,尽量使function的赋值意义更加明确,因此我自己只会在匿名function时使用这种写法。

四、匿名function与管道操作符|>

回到管道操作符上来,如果我们希望能够在使用|>时,实现占位符.的效果,要怎么办呢?答案很简单,就是自己用匿名function写一个。

还是刚才的作图代码,我们来定义一个匿名function替代占位符.的功能好了:

iris |> 
        function(.) {
                plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
                }
Error: function 'function' not supported in RHS call of a pipe

WHAT?!居然还是报错了…让我们来仔细看一下匿名function是如何工作的。

之前我们定义了一个非匿名的function叫做my_func,当调用它时我们需要在my_func的后面加上括号(),也就是说my_func只是这个function的名字,加了括号之后的my_func()才会实际调用这个function,这与R自带的function,例如mean()median()summary()是同理。

仔细观察一下我们定义的匿名function的代码:

function(.) {
        plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
        }

假如我们将这部分代码赋值给一个变量,例如plot_func,实际上它就变成了一个非匿名的function:

plot_func <- function(.) {
        plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
        }

此时如果我们想调用这个plot_func,就必须在后面加上()才可以。

换句话说我们写的匿名function的代码其实只是function的名字部分(相当于没有加括号的plot_func),而调用时则必须在后面加上括号。

那么按照这个思路,刚才报错的代码其实并没有真的调用我们写好的匿名function,我们试试看在后面加上()对它进行调用,会发生什么:

iris |>
        (function(.) {
                plot(Sepal.Width ~ Sepal.Length, col = Species, data = .)
                }
         )() # 这里的括号实现function的调用

成功了!需要注意的是,匿名function的前半部分也需要用一组括号括起来,表示它是一个整体。

如果使用简便写法,则上面的代码可以简化如下:

iris |>
        (\(.) { plot(Sepal.Width ~ Sepal.Length, col = Species, data = .) })()

五、小结

  1. |>%>%均可以用来实现批量管道操作,提高代码书写和运行的效率。

  2. 占位符.只能配合%>%使用,不能配合|>使用。

  3. 可以使用匿名function来替代占位符.实现类似的效果,但需要注意语法结构:

    data |> (function(.) { ... })()

    或者

    data |> (\(.) { ... })()