当使用一个没有空括号的函数时,使用magrittr的管道%>%有什么缺点?

fae0ux8s  于 2023-05-26  发布在  其他
关注(0)|答案(1)|浏览(149)

我使用了%>%magrittr管道),在answer中为RHS提供了一个不带空括号的函数,并得到了一条评论:* 推荐的约定是为RHS提供空括号。*

library(magrittr)

1:3 %>% sum    # The documentation calls this: Basic use

1:3 %>% sum()  # It's also possible to supply empty parentheses
1:3 |> sum()   # And It's similar to |> the base pipe

一个优点可能是,语法类似于|>的基本管道。
但另一方面,%>%也可以像函数一样使用,并且这些函数通常不带括号。

`%>%`(1:3, sum)

sapply(list(1:3), sum)
`%=>%` <- sapply
list(1:3) %=>% sum

do.call(sum, list(1:3))
`%<%` <- do.call
sum %<% list(1:3)

在这种情况下,不带括号使用它看起来像是常量。
另一方面,当使用占位符时,需要提供括号。

"axc" %>% sub("x", "b", .)

我想知道在管道中提供一个没有括号的函数时有什么缺点,以及提供空括号的 * 好的技术原因 * 是什么?

o8x7eapl

o8x7eapl1#

但另一方面,%>%也可以像函数一样使用,这些函数通常不带括号。
不,这是令人困惑的事情:“通常提供”功能的方式并不单一,它完全取决于用途。
我们使用sapplydo.call的例子。两者都是higher-order functions,这意味着它们期望函数作为参数。1由于它们期望函数作为参数,我们可以传递一个引用函数的名称。但是我们也可以传递一个任意的 expression,而不是一个名字,它的计算结果是一个函数。
事实上,不要因为在示例中传递了一个名字而感到困惑,这是一个转移注意力的东西。这里有一个例子,我们传递了一个表达式的结果(返回一个函数):

make_adder = function (y) {
    function (x) x + y
}

sapply(1 : 3, make_adder(2))

但这可能会分散注意力,因为%>%不期望函数对象作为其第二个参数。相反,它需要一个 * 函数调用表达式 *。
在上面的示例中,sapply是一个常规函数,它使用 * 标准求值 * 来计算其参数。它的两个参数1 : 3make_adder(2)都被计算,结果作为参数传递给sapply。2
%>%不是一个常规函数:它抑制了第二参数的标准求值。相反,它将表达式保持为未计算的形式并对其进行操作。它的实现方式相当复杂,但在最简单的情况下,它将第一个参数注入表达式,然后对其求值。下面是一些伪代码来说明这一点:

`%>%` = function (lhs, rhs) {
    # Get the unevaluated expression passed as `rhs`
    rhs_expr = substitute(rhs)
    new_rhs_expr = insert_first_argument_into(rhs_expr, lhs)
    eval.parent(new_rhs_expr)
}

这适用于 * 任何 * 有效的rhs表达式:sum()head(3)%>%分别将它们转换为sum(lhs)sum(lhs, 3)等,并计算结果表达式。
到目前为止,这是完全一致的。然而,%>%的作者选择允许一个 * 额外的 *,完全不同的用法:你也可以传递一个简单的名字,而不是像rhs那样传递函数调用表达式。在这种情况下,%>%会做一些完全不同的事情。它没有构造一个新的调用表达式来注入lhs并对其求值,而是直接调用rhs(lhs)

`%>%` = function (lhs, rhs) {
    rhs_expr = substitute(rhs)

    if (is.name(rhs_expr)) {
        rhs(lhs)
    } else {
        # (code from above.)
    }
}

换句话说,%>%rhs接受两种根本不同类型的参数,并为它们做不同的事情。
这本身还不是一个问题。如果我们传递一个 * 函数工厂 * 作为rhs,这就 * 成为 * 一个问题。这是一个高阶函数,它本身返回一个函数。上面的make_adder就是这样一个函数工厂。
1 : 3 %>% make_adder(2)是什么?...

Error in make_adder(., 2) : unused argument (2)

哦,对了!make_adder(2)是一个函数调用表达式,所以%>%的第一个定义适用:转换表达式并对其求值。所以它试图计算make_adder(2, 1 : 3),但失败了,因为make_adder只需要一个参数。
幸运的是,我们可以将make_adder%>%一起使用。这甚至不需要额外的规则或文档。稍微思考一下,它直接遵循上面的第一个定义:我们需要添加另一层函数调用,因为我们希望%>%调用make_adder * 返回 * 的函数。以下工程:
%>%lhs进行插值,使得new_rhs变为make_adder(2)(1 : 3)
我们可以通过将make_adder(2)的返回值分配给一个名称来使其更具可读性:

add_2 = make_adder(2)

1 : 3 %>% make_adder(2)()      # (1)
#         \___________/
#               v
#             /‾‾‾\
1 : 3 %>%     add_2()          # (2)

我们直接用新引入的名称替换子表达式。这是一个非常基本的计算机科学概念,但它是如此强大,以至于它有自己的名字:referential transparency。这是一个使程序推理更容易的概念,因为我们知道我们总是可以将任意子表达式分配给一个名称,并在一段代码中使用该名称:(1)和(2)是相同的。
但是,实际上,引用透明性要求我们也可以反向进行替换,即。将名称替换为它所引用的值。果然,这是可行的,我们得到了原始的表达式:
(1)和(2)仍然相同。
但不幸的是它并不总是有效
(1)工作,但(2)失败,即使我们只是用它的定义替换add_2%>%不保留引用透明性。3
这就是为什么在RHS上不使用括号是不一致的,也是为什么它被广泛地劝阻(例如。《易经·系辞下》:这也是(据我所知)为什么R核心开发人员决定|>总是需要一个函数调用表达式作为其RHS,并且不能省略括号。
1我们有一个专门的词来描述这个概念,因为接受函数作为参数在主流编程语言中非常罕见。
2这是一种简化。真相更复杂,但与此无关。如果你有兴趣,请参阅R语言定义:论证评估。

3在R中违反引用透明性是很容易的,因为R给了我们很多控制如何计算表达式的控制权。这通常是非常方便的。但是如果不小心使用,它可能会导致代码混乱和细微的错误,建议仔细权衡违反引用透明性的好处。

相关问题