省略号...在替换函数中的作用是什么?

27

我不太理解为什么有时候圆括号会在本应该不能使用的地方起作用®。

f = function(...) substitute(...()); f(a, b)
[[1]]
a
[[2]]
b
# but, substitute returns ..1
f2 = function(...) substitute(...); f2(a, b)
a

通常会抛出错误,例如在调用(\(...) ...())(5)时,会抛出找不到函数"..."'...'在不正确的上下文中使用

我尝试过的
我查看了substitute源代码,以找出为什么这里不会发生。R内部 1.1.1和1.5.2表示...是DOTSXP类型的SEXPTYPE,是一对承诺的pairlist。这些承诺是由substitute提取的。

#  \-substitute #R
#    \-do_substitute #C
#      \-substituteList #C recursive
#        \-substitute #C

逐行查看,我卡在substituteList这里,其中h是正在处理的...的当前元素。在第2832行,if (TYPEOF(h) == DOTSXP) h = substituteList(h, R_NilValue);中发生了递归,我没有在源代码中找到对...()情况的异常处理,因此我怀疑这之前发生了什么。

?substitute中我们发现substitute基于纯词法基础工作。这是否意味着...()是一个parser技巧?

parse(text = "(\\(...) substitute(...()))(a, b)") |> getParseData() |> subset(text == "...", select = c(7, 9))

#>                   token  text
#> 4        SYMBOL_FORMALS   ...
#> 10 SYMBOL_FUNCTION_CALL   ...

第二个省略号被识别为函数调用的名称,在词法分析期间。它没有像 |> 一样有自己的标记。输出是一个成对列表 (typeof(f(a, b))),在这种情况下与常规列表 相同。我猜这不是解析器的技巧。但无论它是什么,它已经存在一段时间了!

old tricks

问题:
...()函数是如何工作的?


1
很好的问题。这是“substitute”未记录的行为。一定很想知道解决方案。 - Onyambu
3
R-devel邮件列表上有一个相关问题:支持substitute(...())。它没有得到真正的回答,是吗? - Henrik
2
@Henrik 是的,那个问题似乎很重要。这是官方API的一部分吗?因为它似乎没有被记录下来。还是这是(间接)记录下来的,我们只是不理解文档?有人应该跟进R-devel。 - Roland
@Henrik 我们可以在某种程度上回答其中一个问题:(function(...) substitute(...()))(a,b) 在版本1.3.1(2001年9月)和1.4.1(2002年1月)之间添加。在1.4.1中,它可以正常运行。已经过去20年了,但仍未被记录(?) - Donald Seinen
好的,?substitute确实会发出警告:“不能保证生成的表达式有任何意义。”我猜这是为了捕捉奇怪行为而设计的。 - Mikael Jagan
1个回答

22

注意:在提到文档和源代码时,我提供了链接到 R 的 官方 Subversion 仓库的一个 非官方 GitHub 镜像。这些链接绑定到 GitHub 仓库中的 提交记录 97b6424,该提交记录对应于 Subversion 仓库中的 修订版本号 81461(此编辑时为最新版本)。


substitute 是一个“特殊”的函数,其参数不会被评估(doc)。

typeof(substitute)

[1] "special"

这意味着substitute的返回值可能与解析器逻辑不一致,具体取决于内部如何处理未评估的参数。

通常情况下,substitute将调用...(<exprs>)作为形式为pairlist(R_DotsSymbol, <exprs>)doc)的LANGSXPsubstitute调用的上下文决定了如何处理SYMSXPR_DotsSymbol。特别地,如果在带有...作为正式参数和rho作为其执行环境的函数内部调用了substitute,则结果为:

findVarInFrame3(rho, R_DotsSymbol, TRUE)

在C语言实用程序的主体中,substituteList (source) 是DOTSXPR_MissingArg,仅当f没有参数调用时才是后者(doc)。在其他情况下,结果是R_UnboundValue或(特殊情况下)一些其他SEXP,仅当在rho中将一个值绑定到名称...时才是后者。substituteList会特殊处理这些情况。 R_DotsSymbol的处理方式不同造成了以下R语句产生不同的结果的原因。
f0 <- function() substitute(...(n = 1)); f0()
## ...(n = 1)
f1 <- function(...) substitute(...(n = 1)); f1()
## $n
## [1] 1
g0 <- function() {... <- quote(x); substitute(...(n = 1))}; g0()
## Error in g0() : '...' used in an incorrect context
g1 <- function(...) {... <- quote(x); substitute(...(n = 1))}; g1()
## Error in g1() : '...' used in an incorrect context
h0 <- function() {... <- NULL; substitute(...(n = 1))}; h0()
## $n
## [1] 1
h1 <- function(...) {... <- NULL; substitute(...(n = 1))}; h1()
## $n
## [1] 1

考虑到...(n = 1)的解析方式,您可能期望f1返回call("...", n = 1)g0g1都返回call("x", n = 1)h0h1都会抛出错误,但由于上述原因大多数未记录,情况并非如此。

内部机制

在R函数f内部调用时,

f <- function(...) substitute(...(<exprs>))

substitute函数调用C语言工具do_substitute,你可以通过查看here来了解这一点。在该函数中,argList得到一个LISTSXP,其形式为pairlist(x, R_MissingArg),其中x是一个LANGSXP,其形式为pairlist(R_DotsSymbol, <exprs>)(source)。

如果您跟随do_substitute的主体,那么您将发现从do_substitute传递给substituteListt的值是一个LISTSXP,其形式为pairlist(copy_of_x) (source)。

由此可知,在substituteList调用的while循环( source )中只有一个迭代,并且循环体中的语句CAR(el) == R_DotsSymbol( source )在该迭代中为false

在条件语句的 false 分支中 (source),h 得到了值 pairlist(substituteList(copy_of_x, env))。循环退出,substituteListh 返回给 do_substitute,然后将 CAR(h) 返回给 R (源 1, 2, 3)。
因此,substitute 的返回值为 substituteList(copy_of_x, env),现在需要推导出这个 SEXP 的身份。在 substituteList 中,while 循环有 1+m 次迭代,其中 m<exprs> 的数量。在第一次迭代中,循环体内的语句 CAR(el) == R_DotsSymboltrue
在条件语句的 true 分支中 (source),h 要么是 DOTSXP,要么是 R_MissingArg,因为 f... 作为形式参数 (doc)。继续查看,您会发现 substituteList 的返回值为:
  • 如果在第一个while迭代中hR_MissingArg,并且m = 0,则返回R_NilValue

否则,返回一个LISTSXP,其中列出了h中的表达式(如果h在第一个while迭代中为DOTSXP),后跟<exprs>(如果m > 1),所有表达式均未求值且没有替换,因为在substitute调用时,f的执行环境为空。

确实:

f <- function(...) substitute(...())
is.null(f())
## [1] TRUE
f <- function(...) substitute(...(n = 1))
identical(f(a = sin(x), b = zzz), pairlist(a = quote(sin(x)), b = quote(zzz), n = 1))
## [1] TRUE

杂项

顺便提一下,为了帮助我更好地理解,我在coerce.c中添加了一些打印语句,并重新编译了R。例如,在do_substitutesource)函数体中的UNPROTECT(3);之前,我添加了以下内容:

    Rprintf("CAR(t) == R_DotsSymbol? %d\n",
            CAR(t) == R_DotsSymbol);
    if (TYPEOF(CAR(t)) == LISTSXP || TYPEOF(CAR(t)) == LANGSXP) {
        Rprintf("TYPEOF(CAR(t)) = %s, length(CAR(t)) = %d\n",
                type2char(TYPEOF(CAR(t))), length(CAR(t)));
        Rprintf("CAR(CAR(t)) = R_DotsSymbol? %d\n",
                CAR(CAR(t)) == R_DotsSymbol);
        Rprintf("TYPEOF(CDR(CAR(t))) = %s, length(CDR(CAR(t))) = %d\n",
                type2char(TYPEOF(CDR(CAR(t)))), length(CDR(CAR(t))));
    }
    if (TYPEOF(s) == LISTSXP || TYPEOF(s) == LANGSXP) {
        Rprintf("TYPEOF(s) = %s, length(s) = %d\n",
                type2char(TYPEOF(s)), length(s));
        Rprintf("TYPEOF(CAR(s)) = %s, length(CAR(s)) = %d\n",
                type2char(TYPEOF(CAR(s))), length(CAR(s)));
    }

这帮助我确认了前一行substituteList调用中输入和输出的内容:

f <- function(...) substitute(...(n = 1))
invisible(f(hello, world, hello(world)))

CAR(t) == R_DotsSymbol? 0
TYPEOF(CAR(t)) = language, length(CAR(t)) = 2
CAR(CAR(t)) = R_DotsSymbol? 1
TYPEOF(CDR(CAR(t))) = pairlist, length(CDR(CAR(t))) = 1
TYPEOF(s) = pairlist, length(s) = 1
TYPEOF(CAR(s)) = pairlist, length(CAR(s)) = 4

invisible(substitute(...()))

CAR(t) == R_DotsSymbol? 0
TYPEOF(CAR(t)) = language, length(CAR(t)) = 1
CAR(CAR(t)) = R_DotsSymbol? 1
TYPEOF(CDR(CAR(t))) = NULL, length(CDR(CAR(t))) = 0
TYPEOF(s) = pairlist, length(s) = 1
TYPEOF(CAR(s)) = language, length(CAR(s)) = 1

显然,使用调试符号编译R并在调试器下运行R也有帮助。

另一个谜题

刚刚注意到这个奇怪的现象:

g <- function(...) substitute(...(n = 1), new.env())
gab <- g(a = sin(x), b = zzz)
typeof(gab)
## [1] "language"
gab
## ...(n = 1)

有人可以进行深入研究,找出为什么当你提供env参数时(包括env = NULL),结果是LANGSXP而不是LISTSXP吗?请注意保留HTML标签。


1
首先,非常巧妙的方法!重新编译R进行故障排除值得拥有自己的主题。在 (\(...) substitute(...))(a, b) 的情况下,只返回 a。从您的答案中,我了解到这是因为 ...()pairlist(R_DotsSymbol, <exprs>)。但是,我不完全清楚为什么没有括号的 ... 不遵循与 1+m 迭代相同的路径,从而产生与 ...() 相同的结果。 - Donald Seinen
2
在这种情况下,在do_substitute中,argList的值为pairlist(R_DotsSymbol, R_MissingArg)而不是pairlist(pairlist(R_DotsSymbol,<exprs>), R_MissingArg)。 (实际上, ...本身就是一个SYMSXP,而...()是一个LANGSXP,因此do_substitute在这里表现一致。)由此可见,substituteList返回到do_substituteLISTSXPpairlist(a,b),而do_substitute返回给R的SYMSXPa - Mikael Jagan

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