我试图理解如何简洁地实现类似于参数捕获/解析/评估机制的东西,它使用dplyr::tibble()
(FKA dplyr::data_frame()
)启用以下行为:
# `b` finds `a` in previous arg
dplyr::tibble(a=1:5, b=a+1)
## a b
## 1 2
## 2 3
## ...
# `b` can't find `a` bc it doesn't exist yet
dplyr::tibble(b=a+1, a=1:5)
## Error in eval_tidy(xs[[i]], unique_output) : object 'a' not found
对于base::
和data.frame
等list
类,这是不可能的(也许bc参数不是顺序解释的(?)和/或bc它们可以在父环境(?)):
data.frame(a=1:5, b=a+1)
## Error in data.frame(a = 1:5, b = a + 1) : object 'a' not found
list(a=1:5, b=a+1)
## Error: object 'a' not found
所以我的问题是:在基础R 中可能是一个好的策略来编写一个与list2()
类似的函数base::list()
除外它允许tibble()
行为list2(a=1:5, b=a+1)
?
我知道这是“tidyeval”所做的一部分,但我有兴趣隔离使这个技巧成为可能的确切机制。而且我知道可以说list(a <- 1:5, b <- a+1)
,但我正在寻找一种不使用全局赋值的解决方案。
到目前为止我一直在想的:实现所需行为的一种不优雅且不安全的方法如下:首先将参数解析为字符串,然后创建一个环境,添加每个元素到那个环境,把它们放到一个列表中,并返回(建议将更好的方法解析为...
到命名列表中赞赏!):
list2 <- function(...){
# (gross bc we are converting code to strings and then back again)
argstring <- as.character(match.call(expand.dots=FALSE))[2]
argstring <- gsub("^pairlist\\((.+)\\)$", "\\1", argstring)
# (terrible bc commas aren't allowed except to separate args!!!)
argstrings <- strsplit(argstring, split=", ?")[[1]]
env <- new.env()
# (icky bc all args must have names)
for (arg in argstrings){
eval(parse(text=arg), envir=env)
}
vars <- ls(env)
out <- list()
for (var in vars){
out <- c(out, list(eval(parse(text=var), envir=env)))
}
return(setNames(out, vars))
}
这允许我们推导出基本行为,但它根本没有很好地概括(参见list2()
定义中的注释):
list2(a=1:5, b=a+1)
## $a
## [1] 1 2 3 4 5
##
## $b
## [1] 2 3 4 5 6
我们可以引入黑客来修复一些小东西,例如在没有提供名称时生成名称,例如:像这样:
# (still gross but at least we don't have to supply names for everything)
list3 <- function(...){
argstring <- as.character(match.call(expand.dots=FALSE))[2]
argstring <- gsub("^pairlist\\((.+)\\)$", "\\1", argstring)
argstrings <- strsplit(argstring, split=", ?")[[1]]
env <- new.env()
# if a name isn't supplied, create one of the form `v1`, `v2`, ...
ctr <- 0
for (arg in argstrings){
ctr <- ctr+1
if (grepl("^[a-zA-Z_] ?= ?", arg))
eval(parse(text=arg), envir=env)
else
eval(parse(text=paste0("v", ctr, "=", arg)), envir=env)
}
vars <- ls(env)
out <- list()
for (var in vars){
out <- c(out, list(eval(parse(text=var), envir=env)))
}
return(setNames(out, vars))
}
然后代替:
# evaluates `a+b-2`, but doesn't include in `env`
list2(a=1:5, b=a+1, a+b-2)
## $a
## [1] 1 2 3 4 5
##
## $b
## [1] 2 3 4 5 6
我们得到了这个:
list3(a=1:5, b=a+1, a+b-2)
## $a
## [1] 1 2 3 4 5
##
## $b
## [1] 2 3 4 5 6
##
## $v3
## [1] 1 3 5 7 9
但即使我们用逗号,名字等修复问题,感觉仍然会出现问题边缘案例。
任何人都有任何想法/建议/见解/解决方案等等。
非常感谢!
答案 0 :(得分:2)
data.frame(a=1:5, b=a+1)
无效的原因是范围问题,而不是评估订单问题。
通常在调用帧中计算函数的参数。当您说a+1
时,您指的是调用a
的框架中的变量data.frame
,而不是您要创建的列。
dplyr::data_frame
执行非常非标准的评估,因此可以按照您的看法混合帧。它似乎首先在与正在构造的对象相对应的框架中查找,在通常的位置中第二个。
将dplyr
语义与基函数一起使用的一种方法是同时执行这两种操作,
e.g。
do.call(data.frame, as.list(dplyr::data_frame(a = 1:5, b = a+1)))
但这有点无用:你可以直接将一个tibble转换为数据帧,而这不能与其他基本函数一起使用,因为它强制所有参数都有相同的长度。
要编写list2
函数,我建议您查看dplyr::data_frame
的来源,并执行除最终转换为tibble之外的所有操作。它的来源看似简短:
function (...)
{
xs <- quos(..., .named = TRUE)
as_tibble(lst_quos(xs, expand = TRUE))
}
这具有欺骗性,因为lst_quos
是tibble
包中的私有函数,因此您需要自己的副本,以及它调用的任何私有函数,等等。当然你不介意使用私人功能,然后在这里list2
:
list2 <- function(...) {
xs <- rlang::quos(..., .named = TRUE)
tibble:::lst_quos(xs, expand = TRUE)
}
这将有效,直到tibble
维护者选择更改lst_quos
,他可以在没有警告的情况下自由地进行更改{因为它是私有的)。由于这种脆弱性,它不会是CRAN包中可接受的代码。