R的原生管道`|>`和magrittr管道`%>%`有什么区别?

109
在 R 4.1(2021 年 5 月)中引入了一种原生管道操作符,比之前的实现更加“简洁”。我已经注意到原生的 |> 和 magrittr 管道 %>% 之间的一个区别,即 2 %>% sqrt 是有效的,但 2 |> sqrt 不行,必须写成 2 |> sqrt()。在使用原生管道操作符时,还有其他的区别和注意事项吗?

8
你看过?pipeOp?"%>%"帮助页面了吗?那是一个很好的信息来源。 - MrFlick
关于此主题的官方tidyverse博客文章:https://www.tidyverse.org/blog/2023/04/base-vs-magrittr-pipe/ - undefined
5个回答

100
主题 Magrittr 2.0.3 Base 4.3.0
运算符 %>% %<>% %$% %!>% %T>% |> (自4.1.0起)
函数调用 1:3 %>% sum() 1:3 |> sum()
  1:3 %>% sum 需要括号
  1:3 %>% `+`(4) 不支持某些函数
插入到第一个空位置 mtcars %>% lm(formula = mpg ~ disp) mtcars |> lm(formula = mpg ~ disp)
占位符 . _ (自4.2.0起)
  mtcars %>% lm(mpg ~ disp, data = . ) mtcars |> lm(mpg ~ disp, data = _ )
  mtcars %>% lm(mpg ~ disp, . ) 需要命名参数
  1:3 %>% setNames(., .) 只能出现一次
  1:3 %>% {sum(sqrt(.))} 不允许嵌套调用
提取调用 mtcars %>% .$cyl
mtcars %>% {.$cyl[[3]]}
mtcars %$% cyl[[3]]
mtcars |> _$cyl (自4.3.0起)
mtcars |> _$cyl[[3]]

环境 %>%额外的函数环境
使用:"x" %!>% assign(1)
"x" |> assign(1)
创建函数 top6 <- . %>% sort() %>% tail() 不可能
速度 较慢,因为函数调用的开销 较快,因为语法转换
许多差异和限制在使用|>与(匿名)函数结合时消失了:
1 |> (\(.) .)()
-3:3 |> (\(.) sum(2*abs(.) - 3*.^2))()

还可以参考:如何在基本R中进行纯粹的管道操作('base pipe')?五个Magrittr管道符号%>%,%<>%,%$%,%!>%和%T>%的区别和用法是什么?


需要括号

library(magrittr)

1:3 |> sum
#Error: The pipe operator requires a function call as RHS

1:3 |> sum()
#[1] 6

1:3 |> approxfun(1:3, 4:6)()
#[1] 4 5 6

1:3 %>% sum
#[1] 6

1:3 %>% sum()
#[1] 6

1:3 %>% approxfun(1:3, 4:6)  #But in this case empty parentheses are needed
#Error in if (is.na(method)) stop("invalid interpolation method") :
1:3 %>% approxfun(1:3, 4:6)()
#[1] 4 5 6

有些功能不被支持的, 但是有些仍然可以通过将它们放在括号中来调用,通过函数::调用它们,使用占位符,在函数中调用它,或者定义一个指向该函数的链接。
1:3 |> `+`(4)
#Error: function '+' not supported in RHS call of a pipe

1:3 |> (`+`)(4)
#[1] 5 6 7

1:3 |> base::`+`(4)
#[1] 5 6 7

1:3 |>  `+`(4, e2 = _)
#[1] 5 6 7

1 |> (`+`)(2) |> (`*`)(3) #(1 + 2) * 3  or `*`(`+`(1, 2), 3) and NOT 1 + 2 * 3
#[1] 9

1:3 |> (\(.) . + 4)()
#[1] 5 6 7

fun <- `+`
1:3 |> fun(4)
#[1] 5 6 7

1:3 %>% `+`(4)
#[1] 5 6 7

占位符需要命名参数
2 |> setdiff(1:3, _)
#Error: pipe placeholder can only be used as a named argument

2 |> setdiff(1:3, y = _)
#[1] 1 3

2 |> (\(.) setdiff(1:3, .))()
#[1] 1 3

2 %>% setdiff(1:3, .)
#[1] 1 3

2 %>% setdiff(1:3, y = .)
#[1] 1 3

对于带有“...”(点点点)参数的可变参数函数,还需要使用占位符“_”作为命名参数。
"b" |>  paste("a", _, "c")
#Error: pipe placeholder can only be used as a named argument

"b" |>  paste("a", . = _, "c")
#[1] "a b c"

"b" |>  (\(.) paste("a", ., "c"))()
#[1] "a b c"

占位符只能出现一次。
1:3 |> setNames(nm = _)
#1 2 3 
#1 2 3 

1:3 |> setNames(object = _, nm = _)
#Error in setNames(object = "_", nm = "_") : 
#  pipe placeholder may only appear once

1:3 |> (\(.) setNames(., .))()
#1 2 3 
#1 2 3 

1:3 |> list() |> setNames(".") |> with(setNames(., .))
#1 2 3 
#1 2 3 

1:3 |> list(. = _) |> with(setNames(., .))
#1 2 3
#1 2 3

1:3 %>% setNames(object = ., nm = .)
#1 2 3
#1 2 3

1:3 %>% setNames(., .)
#1 2 3 
#1 2 3

嵌套调用是不允许的。
1:3 |> sum(sqrt(x=_))
#Error in sum(1:3, sqrt(x = "_")) : invalid use of pipe placeholder

1:3 |> (\(.) sum(sqrt(.)))()
#[1] 4.146264

1:3 %>% {sum(sqrt(.))}
#[1] 4.146264

提取调用 自4.3.0版本以来的实验性功能。占位符_现在也可以在前向管道|>表达式的rhs中作为提取调用的第一个参数使用,例如_$coef。更一般地,它可以作为一系列提取的头部使用,例如_$coef[[2]]
mtcars |> _$cyl
mtcars |> _[["cyl"]]
mtcars |> _[,"cyl"]
# [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4

mtcars |> _$cyl[[4]]
#[1] 6

mtcars %>% .$cyl
mtcars %>% .[["cyl"]]
mtcars %>% .[,"cyl"]
# [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4

#mtcars %>% .$cyl[4] #gives mtcars[[4]]
mtcars %>% .$cyl %>% .[4]
#[1] 6

没有额外的环境
assign("x", 1)
x
#[1] 1

"x" |> assign(2)
x
#[1] 2

"x" |> (\(x) assign(x, 3))()
x
#[1] 2

1:3 |> assign("x", value=_)
x
#[1] 1 2 3

"x" %>% assign(4)
x
#[1] 1 2 3

4 %>% assign("x", .)
x
#[1] 1 2 3

"x" %!>% assign(4) #Use instead the eager pipe
x
#[1] 4

5 %!>% assign("x", .)
x
#[1] 5

创建一个函数
top6 <- . %>% sort() %>% tail()
top6(c(1:10,10:1))
#[1]  8  8  9  9 10 10

其他可能性:
可以通过使用奇异管道->.;来实现不同的管道操作符和不同的占位符,这不是一个管道(参见缺点),它会覆盖.

1:3 ->.; sum(.)
#[1] 6

mtcars ->.; .$cyl
# [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4

mtcars ->.; .$cyl[4]
#[1] 6

1:3 ->.; setNames(., .)
#1 2 3 
#1 2 3 

1:3 ->.; sum(sqrt(x=.))
#[1] 4.146264

"x" ->.; assign(., 5)
x
#[1] 5

6 ->.; assign("x", .)
x
#[1] 6

1:3 ->.; . + 4
#[1] 5 6 7

1 ->.; (`+`)(., 2) ->.; (`*`)(., 3)
#[1] 9

1 ->.; .+2 ->.; .*3
#[1] 9

评估不同。
x <- data.frame(a=0)
f1 <- \(x) {message("IN 1"); x$b <- 1; message("OUT 1"); x}
f2 <- \(x) {message("IN 2"); x$c <- 2; message("OUT 2"); x}

x ->.; f1(.) ->.; f2(.)
#IN 1
#OUT 1
#IN 2
#OUT 2
#  a b c
#1 0 1 2

x |> f1() |> f2()
#IN 2
#IN 1
#OUT 1
#OUT 2
#  a b c
#1 0 1 2

f2(f1(x))
#IN 2
#IN 1
#OUT 1
#OUT 2
#  a b c
#1 0 1 2

或者定义一个自定义的管道操作符,将`.`设置为lhs的值,并在新环境中评估rhs。但是在这种情况下,无法创建或更改调用环境中的值。
`:=` <- \(lhs, rhs) eval(substitute(rhs), list(. = lhs))

mtcars := .$cyl[4]
#[1] 6

1:3 := setNames(., .)
#1 2 3 
#1 2 3 

1:3 := sum(sqrt(x=.))
#[1] 4.146264

"x" := assign(., 6)
x
#Error: object 'x' not found

1 := .+2 := .*3
#[1] 9

所以另一种尝试是将lhs赋值给调用环境中的占位符.,并在调用环境中评估rhs。但是在这种情况下,如果.已经存在于调用环境中,它将被移除。
`?` <- \(lhs, rhs) {
  on.exit(if(exists(".", parent.frame())) rm(., envir = parent.frame()))
  assign(".", lhs, envir=parent.frame())
  eval.parent(substitute(rhs))
}

mtcars ? .$cyl[4]
#[1] 6

1:3 ? setNames(., .)
#1 2 3 
#1 2 3 

1:3 ? sum(sqrt(x=.))
#[1] 4.146264

"x" ? assign(., 6)
x
#[1] 6

1 ? .+2 ? .*3
#[1] 9

另一种可能性是将所有的.替换为lhs,这样在评估过程中.就不再存在作为一个名称。
`%|>%` <- \(lhs, rhs)
  eval.parent(eval(call('substitute', substitute(rhs), list(. = lhs))))

mtcars %|>% .$cyl[4]
[1] 6

1:3 %|>% setNames(., .)
1 2 3 
1 2 3

1:3 %|>% sum(sqrt(x=.))
[1] 4.146264

"x" %|>% assign(., 6)
x
#[1] 6

1 %|>% .+2 %|>% .*3
#[1] 7

使用的运算符名称会影响运算符的优先级:参见相同功能,但使用名称 %>% 会导致与使用名称 := 时不同的结果
有关更高级选项,请参见:编写自己的/自定义管道运算符

Speed

library(magrittr)

`:=` <- \(lhs, rhs) eval(substitute(rhs), list(. = lhs))

`?` <- \(lhs, rhs) {
  on.exit(if(exists(".", parent.frame())) rm(., envir = parent.frame()))
  assign(".", lhs, envir=parent.frame())
  eval.parent(substitute(rhs))
}

`%|>%` <- \(lhs, rhs)
  eval.parent(eval(call('substitute', substitute(rhs), list(. = lhs))))


x <- 42
bench::mark(min_time = 0.2, max_iterations = 1e8
, x
, identity(x)
, "|>" = x |> identity()
, "|> _" = x |> identity(x=_)
, "->.;" = {x ->.; identity(.)}
, "|> f()" = x |> (\(y) identity(y))()
, "%>%" = x %>% identity
, ":=" = x := identity(.)
, "list." = x |> list() |> setNames(".") |> with(identity(.))
, "%|>%" = x %|>% identity(.)
, "?" = x ? identity(.)
)

结果

   expression       min   median `itr/sec` mem_alloc `gc/sec`   n_itr  n_gc
   <bch:expr>  <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>   <int> <dbl>
 1 x            31.08ns   48.2ns 19741120.        0B     7.46 2646587     1
 2 identity(x) 491.04ns 553.09ns  1750116.        0B    27.0   323575     5
 3 |>          497.91ns 548.08ns  1758553.        0B    27.3   322408     5
 4 |> _        506.87ns 568.92ns  1720374.        0B    26.9   320003     5
 5 ->.;        725.03ns 786.04ns  1238488.        0B    21.2   233864     4
 6 |> f()      972.07ns   1.03µs   929926.        0B    37.8   172288     7
 7 %>%           2.76µs   3.05µs   315448.        0B    37.2    59361     7
 8 :=            3.02µs   3.35µs   288025.        0B    37.0    54561     7
 9 list.         5.19µs   5.89µs   166721.        0B    36.8    31752     7
10 %|>%          6.01µs   6.86µs   143294.        0B    37.0    27076     7
11 ?             30.9µs  32.79µs    30074.        0B    31.3     5768     6

4
非常全面的回复,谢谢让我现在更有信心安全地使用新的本地管道。它使我的代码看起来整洁,与 magrittr 管道相比也少了一次按键。 - Faustin Gashakamba
3
令人惊叹的全面。注意,1:3 |> list() |> setNames(".") |> with(setNames(., .)) 可以写成 1:3 |> list(. = _) |> with(setNames(., .)) 或者甚至是 1:3 |> setNames(nm = _) - G. Grothendieck
谢谢您还使用了“_”占位符!也许我应该改用另一个函数,因为在这里使用setNames时没有必要在两个地方都使用占位符。 - GKi

74
在R 4.1中,本地管道没有占位符语法。因此,没有magrittr的.占位符的等效物,因此以下内容在|>中是不可能的。
c("dogs", "cats", "rats") %>% grepl("at", .)
#[1] FALSE  TRUE  TRUE

自R 4.2版本以来,本地管道可以使用_作为占位符,但仅限于命名参数
c("dogs", "cats", "rats") |> grepl("at", x = _)
#[1] FALSE  TRUE  TRUE

. 和 magrittr 仍然更加灵活,因为 . 可以重复出现并且可以出现在表达式中。

c("dogs", "cats", "rats") %>% 
  paste(., ., toupper(.)) 
#[1] "dogs dogs DOGS" "cats cats CATS" "rats rats RATS"

c("dogs", "cats", "rats") |>
  paste(x = "no", y = _) 
# Error in paste(x = "_", y = "_") : pipe placeholder may only appear once

使用带有未命名可变参数的函数(即...)与|>的方法也不清楚。 在这个paste()示例中,我们可以编造xy参数来欺骗占位符放置在正确的位置,但这感觉很不专业。

c("dogs", "cats", "rats") |>
  paste(x = "no", y = _) 
#[1] "no dogs" "no cats" "no rats"

以下是绕过占位符限制的其他方法:

  1. 编写单独的函数
find_at = function(x) grepl("at", x)
c("dogs", "cats", "rats") |> find_at()
#[1] FALSE  TRUE  TRUE
  1. Use an anonymous function

    a) Use the "old" syntax

    c("dogs", "cats", "rats") |> {function(x) grepl("at", x)}()
    

    b) Use the new anonymous function syntax

    c("dogs", "cats", "rats") |> {\(x) grepl("at", x)}()
    
  2. Specify the first parameter by name. This relies on the fact that the native pipe pipes into the first unnamed parameter, so if you provide a name for the first parameter it "overflows" into the second (and so on if you specify more than one parameter by name)

c("dogs", "cats", "rats") |> grepl(pattern="at")
#> [1] FALSE  TRUE  TRUE

亲爱的Shah Ronak,当我使用本地基础R管道时,我的Rstudio会出现意外的令牌符号,我想知道您是否有任何想法是什么原因导致这种情况。 - Anoushiravan R
1
是的,我已经更新了我的Rstudio到最新版本。 - Anoushiravan R
是的,你说得对!那很奇怪。我根本没有复制粘贴,但我不明白为什么代码在意外符号仍然存在的情况下可以正常运行。 - Anoushiravan R
Dirk Eddelbuettel 的解决方案在使用 R 基础管道时更加优雅:c("dogs", "cats", "rats") |> d => grepl("at", d) - Wang
为什么 |> 不像 %>% 一样允许使用 .?这样会更简洁和好用。 - GitHunter0
2
@GitHunter0 现在已经可以了,使用 _. 请参见此答案 - Maël

43

在 R 4.1.0 中新增的基础管道符 |> 只是函数组合。也就是说,我们可以看到它的使用实际上与函数调用完全相同

> 1:5 |> sum()             # simple use of |>
[1] 15
> deparse(substitute( 1:5 |> sum() ))
[1] "sum(1:5)"
> 

这会产生一些影响:

  • 它使代码稍微快一些
  • 它使代码稍微更简单和更健壮
  • 它使sum()需要括号进行正确调用
  • 它限制了'implicit'数据参数的使用

这导致可能使用当前为“available but not active”的=>(需要设置环境变量_R_USE_PIPEBIND_),并且这可能会在R 4.2.0中发生更改。

(这最初是作为回答重复此问题的答案提供的,并且我只是按照建议复制了它。)

编辑:随着关于“什么是=>”的后续问题出现,这里有一个快速跟进。 请注意,此运算符可能会发生变化。

> Sys.setenv("_R_USE_PIPEBIND_"=TRUE)
> mtcars |> subset(cyl == 4) |> d => lm(mpg ~ disp, data = d)

Call:
lm(formula = mpg ~ disp, data = subset(mtcars, cyl == 4))

Coefficients:
(Intercept)         disp  
     40.872       -0.135  

> deparse(substitute(mtcars |> subset(cyl==4) |> d => lm(mpg ~ disp, data = d)))
[1] "lm(mpg ~ disp, data = subset(mtcars, cyl == 4))"
> 

deparse(substitute(...))在这里特别好用。


隐式数据参数是来自 magrittr 的 .?你所说的 => 可能的用法是什么?pipebind 是否类似于 %<>% - qwr
是的。有一些例子可以使用 => 将数据元素 显式地 分配给一个命名变量,比如 x,并在 lm(...., data=x) 中使用它。 - Dirk Eddelbuettel
1
我仍然不确定 => 是做什么的,但我想这是另一个问题。 - qwr
没错,而且这也是暂时的。但我会快速编辑一下。 - Dirk Eddelbuettel

35

本地管道的实现是通过语法转换来实现的,因此2 |> sqrt()sqrt(2)相比没有明显的开销,而2 %>% sqrt()会带来一些小的惩罚。

microbenchmark::microbenchmark(
  sqrt(1), 
  2 |> sqrt(), 
  3 %>% sqrt()
)

# Unit: nanoseconds
#          expr  min     lq    mean median   uq   max neval
#       sqrt(1)  117  126.5  141.66  132.0  139   246   100
#       sqrt(2)  118  129.0  156.16  134.0  145  1792   100
#  3 %>% sqrt() 2695 2762.5 2945.26 2811.5 2855 13736   100

你看到了将表达式2 |> sqrt()传递给microbenchmark,它被解析为sqrt(2)。这也可以在中看到。

quote(2 |> sqrt())
# sqrt(2)

18

一个区别是它们的占位符,基础R使用_,而magrittr使用.


自从 R 4.2.0版本开始,基础R管道有了一个占位符,_,类似于 %>% 使用的.,但其仅限用于命名参数,并且每次调用只能使用一次。

现在可以在rhs调用中使用带有占位符_的命名参数,以指定lhs应插入的位置。 占位符只能出现在rhs上一次。

重申Ronak Shah的例子,现在可以将_用作右侧的命名参数,以引用公式左侧:

c("dogs", "cats", "rats") |> 
    grepl("at", x = _)
#[1] FALSE  TRUE  TRUE

但它必须被命名为:

c("dogs", "cats", "rats") |> 
    grepl("at", _)
#Error: pipe placeholder can only be used as a named argument

并且不能出现超过一次(为了解决这个问题,人们仍然可以使用Ronak Shah提供的解决方案):

c("dogs", "cats", "rats") |> 
  expand.grid(x = _, y = _)
# Error in expand.grid(x = "_", y = "_") : pipe placeholder may only appear once

虽然这是使用 magrittr 可能的:

library(magrittr)
c("dogs", "cats", "rats") %>% 
  expand.grid(x = ., y = .)
#     x    y
#1 dogs dogs
#2 cats dogs
#3 rats dogs
#4 dogs cats
#5 cats cats
#6 rats cats
#7 dogs rats
#8 cats rats
#9 rats rats

4
限制只能使用一次有意义吗?他们有计划取消这个限制吗? - GitHunter0
3
如果我必须猜测(我不是R-core),那就是因为这些运算符(|>等)重写了语法,使得 longcalc() |> quux(x = _) 变成了 quux(x = longcalc()),而他们不希望 longcalc() |> quux(x=_, y=) 被翻译成双倍计算的 quux(x=longcalc(), y=longcalc())(其中第二个是多余和双倍时间调用)。只是一个猜测。@GitHunter0 - r2evans

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接