Bizarro管道 ->.;是否存在缺点,使其不建议使用?

10

自 R 版本 4.1.0 起,管道符 |> 已经成为稳定版本的一部分。当将 lhs 传递到除第一个参数外的其他参数时,手册中的示例如下:

mtcars |> subset(cyl == 4) |> (function(d) lm(mpg ~ disp, data = d))()

或者在使用\(x)

mtcars |> subset(cyl == 4) |> (\(d) lm(mpg ~ disp, data = d))()

或者使用PIPEBIND,目前需要激活:

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

可以使用Bizarro pipe ->.; 替换 |> 如下:

mtcars |> subset(cyl == 4) ->.; lm(mpg ~ disp, data = .)

在R中,管道符号的一个目的是允许以一种可能使处理步骤顺序更易于理解的方式编写嵌套的调用序列,至少对我来说,->.;也满足了这个要求。Bizarro管道不是一个真正的管道,但对我来说,它目前是一个受欢迎的替代品,尤其是在将lhs传递到除第一个参数以外的参数时。但当我使用它时,我得到评论不要使用它。

因此,我想知道Bizarro管道是否有缺点,推荐不要使用它?


到目前为止,我看到它在环境中创建或覆盖.并保留此引用,这将在修改时强制进行复制。但是,当调用带有数据参数的函数时,还会创建对此数据的引用。而且当使用for循环后,var会留下。

for(i in iris) {}
tracemem(i) == tracemem(iris[[ncol(iris)]])
#[1] TRUE

此外,就性能而言,它并没有显示出太多的不利影响:

x <- 42
library(magrittr)
Sys.setenv(`_R_USE_PIPEBIND_` = TRUE) 
#Nonsense operation to test Performance
bench::mark(x
, identity(x)
, "x |> identity()" = x |> identity()
, "x |> (\\(y) identity(y))()" = x |> (\(y) identity(y))()
, "x |> . => identity(.)" = x |> . => identity(.)
, "x ->.; identity(.)" = {x ->.; identity(.)}
, x %>% identity
)
#  expression                     min   median `itr/sec` mem_alloc `gc/sec` n_itr
#  <bch:expr>                <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int>
#1 x                          60.07ns  69.03ns 13997474.        0B      0   10000
#2 identity(x)               486.96ns 541.91ns  1751206.        0B    175.   9999
#3 x |> identity()           481.03ns 528.06ns  1812935.        0B      0   10000
#4 x |> (\(y) identity(y))() 982.08ns   1.08µs   854349.        0B     85.4  9999
#5 x |> . => identity(.)     484.06ns 528.06ns  1815336.        0B      0   10000
#6 x ->.; identity(.)        711.07ns 767.99ns  1238658.        0B    124.   9999
#7 x %>% identity              2.86µs   3.23µs   294945.        0B     59.0  9998

这个问题似乎只关注性能。正如我在我的回答中所解释的那样,性能并不是问题。而且我有些困惑你会立即想到那个。 - Konrad Rudolph
1
一个人也可以这样写:mtcars |> subset(cyl == 4) |> lm(formula = mpg ~ disp)。我认为性能是重点的原因是它似乎是 |> 的主要动机因素,并且它放弃了大部分 %>% 和 bizarro pipe 的特性,以通过语法转换来实现它。我怀疑关于 bizarro pipe 的副作用参数是否真的具有实际重要性。 - G. Grothendieck
@G.Grothendieck 是的,当占位符之前的所有参数都被填满时,可以避免使用占位符。但有时这会导致很多写作。 - GKi
@G.Grothendieck 我希望能够说服人们:故意编写比可能性更低的健壮代码只是一个极其糟糕的想法:请记住软件工程格言“百万分之一的错误会在下周二出现”:由于规模的原因,不太可能的错误常常会引起问题。并且应该始终最小化悄无声息地产生错误结果的隐藏错误的机会。 - Konrad Rudolph
1个回答

10
bizarro管道的主要问题是它会产生隐藏的副作用,使得创建微妙的错误更加容易。这降低了代码的可维护性。 .变量的持久存在使得在后面意外引用该值变得非常容易:如果你在某个时刻忘记对它进行赋值并认为自己已经赋值了,那么它的存在会掩盖错误。很容易忽视这种可能性,但是这种错误相当常见,而且更糟糕的是,非常不明显:你不会得到错误消息,只会得到错误的结果。相比之下,如果你在某个地方忘记了管道符号,你会立即得到一个错误消息。
更糟糕的是,bizarro管道以两种不同的方式隐藏了这种容易出错的副作用。首先,因为它使得赋值不明显。我之前曾经争论过,不应该使用->赋值,因为从左到右的赋值会隐藏副作用,而副作用应该在语法上明显地表现出来。在这种情况下,副作用就是赋值,在表达式的第一列中最突出的位置发生,而不是在其末尾隐藏。这是对使用->(或任何其他试图掩盖副作用的方法)的基本反对意见,不仅限于bizarro管道。
其次,因为.默认是隐藏的(从ls和IDE的检查器窗格中都看不到),这使得意外依赖它变得更加容易。
因此,如果你想给一个临时名称赋值而不是使用管道,请直接这样做。但是:
  1. 执行从右到左的赋值,即使用name = valuename <- value,而不是value -> name
  2. 使用一个描述性的名称。
我无法强调这是微妙错误的实际来源了——不要低估它!
另一个问题是它的使用破坏了编辑器支持自动格式化代码。这在一些IDE中可以通过插件“解决”,但是解决方案实际上解决了本来不应该存在的问题。为了澄清我的意思,如果你正在使用bizarro管道,你可能希望有一个悬挂缩进,即以下行的格式:
mtcars ->.
  subset(cyl == 4) ->.
  lm(mpg ~ disp, data = .)

...但是自动缩进不会像这样缩进代码,而自动格式化程序将使悬挂缩进变平。这两个问题都不是禁制性的(尽管第一个问题非常严重),但在没有正面的理由支持使用反常的管道运算符的情况下,它们明显地倾向于负面。毕竟,反常的管道运算符解决了什么问题,这些问题不是通过正确的管道运算符或常规赋值更好地解决的呢?如果您不能使用R 4.1,则使用“magrittr”。如果您不喜欢“magrittr”的语义,请编写自己的管道运算符,使用许多其他现有的实现之一,或者只使用常规赋值。最后,有人可能会认为这段代码足够特殊,以至于会让读者感到困惑,但是老实说,如果使用方式一致并在某个地方明确记录,则我认为这不是一个非常令人信服的论点。但这对于推荐初学者使用它来说又是另一个反对的理由。

1 当然,这个问题很容易回答:|> 不允许显式点替换。虽然我理解反对支持它的论点,但其缺失鼓励使用类似奇怪的管道的 hack,这是一个非常有力的论据,表明这实际上是一个巨大的错误。


谢谢你详细的回答!我最近发现存在一个需要激活的管道绑定(pipebind),它允许替换操作。我已经在问题中包含了这种方法,这有助于避免使用 ->;. - GKi
@GKi 我在邮件列表中没有找到任何信息,但我认为 pipebind 要求通过环境变量激活的原因是该功能是实验性的,并且可能会在未来版本中被删除。几乎没有人喜欢邮件列表上的 pipebind 功能,它是一个非常奇怪的东西。 - Konrad Rudolph
是的,目前看来最好不要使用|> =>,因为它可能会发生变化。但至少这意味着他们正在努力,将来基础库中可能会有带有替代的管道功能。 - GKi

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