R

时间:2018-04-21 21:16:48

标签: r dplyr tibble nse

我试图理解如何简洁地实现类似于参数捕获/解析/评估机制的东西,它使用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.framelist类,这是不可能的(也许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

但即使我们用逗号,名字等修复问题,感觉仍然会出现问题边缘案例。

任何人都有任何想法/建议/见解/解决方案等等。

非常感谢!

1 个答案:

答案 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_quostibble包中的私有函数,因此您需要自己的副本,以及它调用的任何私有函数,等等。当然你不介意使用私人功能,然后在这里list2

list2 <- function(...) {
     xs <- rlang::quos(..., .named = TRUE)
     tibble:::lst_quos(xs, expand = TRUE)
}

这将有效,直到tibble维护者选择更改lst_quos,他可以在没有警告的情况下自由地进行更改{因为它是私有的)。由于这种脆弱性,它不会是CRAN包中可接受的代码。