但是另一方面,%>%
也可以像函数一样使用,并且这些函数通常不带括号地提供.
不,这是令人困惑的:没有单一的方式来"典型地"提供函数,它完全取决于使用情况.
您可以使用sapply
和do.call
的例子.两者都是higher-order functions,这意味着它们期望函数作为参数.1因为它们期望函数作为参数,所以我们可以传递一个引用函数的名称.但我们也可以不传递名称,而是传递一个任意的expression,它的计算结果是一个函数.
…事实上,不要因为您在示例中传递了一个名称而感到不安,它是一个转移注意力的问题.以下是我们传递表达式(返回函数)的结果的示例:
make_adder = function (y) {
function (x) x + y
}
sapply(1 : 3, make_adder(2))
但这可能会分散注意力,因为%>%
不希望函数对象作为它的第二个参数.相反,它预计会有function call expression分.
在上面的示例中,sapply
是一个常规函数,它使用standard evaluation计算其参数.计算其参数1 : 3
和make_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)
}
这适用于any个有效的rhs
表达式:sum()
、head(3)
等.%>%
将它们分别转换为sum(lhs)
、sum(lhs, 3)
等,并计算结果表达式.
到目前为止,这是完全一致的.然而,%>%
的作者 Select 允许使用additional,这是完全不同的用法:您也可以传递一个简单的名称,而不是将函数调用表达式作为rhs
传递.在这种情况下,%>%
做的事情完全不同.它不是构造注入lhs
的新调用表达式并对其求值,而是直接调用rhs(lhs)
:
`%>%` = function (lhs, rhs) {
rhs_expr = substitute(rhs)
if (is.name(rhs_expr)) {
rhs(lhs)
} else {
# (code from above.)
}
}
换句话说,%>%
接受两种根本不同类型的论点作为rhs
,并为它们做不同的事情.
这本身还不是问题.如果我们把function factory作为rhs
分,那becomes就是个问题了.这是一个高阶函数,它本身返回一个函数.make_adder
从上面就是这样一个功能工厂.
那么:1 : 3 %>% make_adder(2)
是做什么的?…
Error in make_adder(., 2) : unused argument (2)
哦,对了!make_adder(2)
是一个函数调用表达式,所以%>%
的第一个定义适用:转换表达式并计算它.所以它try 计算make_adder(2, 1 : 3)
,但失败了,因为make_adder
只需要一个参数.
幸运的是,为了我们的理智,我们can用make_adder
加%>%
.这甚至不需要额外的规则或文档.稍微想一想,它直接来自上面的第一个定义:我们需要添加另一层函数调用,因为我们希望%>%
调用returned乘以make_adder
的函数.以下是工作原理:
1 : 3 %>% make_adder(2)()
# 3 4 5
%>%
对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 : 3 %>% add_2() # (1)
# \___/
# v
# /‾‾‾‾‾‾‾‾‾‾‾\
1 : 3 %>% make_adder(2)() # (2)
(1)和(2)仍然相同.
但不幸的是,it does not always work:
1 : 3 %>% add_2 # (1)
# \___/
# v
# /‾‾‾‾‾‾‾‾‾‾‾\
1 : 3 %>% make_adder(2) # (2)
(1)有效,但(2)失败,即使我们只用它的定义替换了add_2
.%>%
不保留引用透明度.3
这就是为什么在RHS上不使用括号是不一致的,也是为什么它被广泛反对(例如by the tidyverse style guide)的原因.这也是(据我所知)R核心开发人员决定|>
总是需要函数调用表达式作为其RHS的原因,并且您不能省略括号.
1我们有一个专门的词来描述这个概念,因为接受函数作为参数在主流编程语言中曾经非常少见.
2这是一种简化.真相更为复杂,但在这里却无关紧要.如果你很好奇,看看R Language Definition: Argument evaluation.
3违反R中的引用透明度非常容易,因为R让我们可以很好地控制如何计算表达式.这通常是非常方便的.但如果不小心使用,它可能会导致混乱的代码和细微的错误,建议仔细权衡违反引用透明度的情况和好处.