【问题标题】:Sequential evaluation of named arguments in RR中命名参数的顺序评估
【发布时间】:2018-10-02 06:31:22
【问题描述】:

我试图了解如何简洁地实现参数捕获/解析/评估机制之类的东西,该机制通过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

对于像data.framelist 这样的base:: 类,这是不可能的(可能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 中编写一个类似于 base::list() 的函数可能是一个好的策略,除了它允许 @ 987654330@ 行为类似于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() 定义中的 cmets):

list2(a=1:5, b=a+1)
## $a
## [1] 1 2 3 4 5
## 
## $b
## [1] 2 3 4 5 6

我们可以引入 hack 来修复一些小问题,例如在未提供名称时生成名称,例如像这样:

# (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

但感觉还是会有问题的边缘情况,即使我们用逗号、名称等解决问题。

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

非常感谢!

【问题讨论】:

  • 这不是一个答案,但一个非常简洁的相关函数是alist。绝对值得一读帮助手册。
  • 确实,这可能对解析 ... args 有用——谢谢。基础 R 中有这么多奇怪的隐藏宝石:p
  • 是的,20 多年的各种添加可能会导致一些迷宫般的搜索。

标签: r dplyr tibble nse


【解决方案1】:

data.frame(a=1:5, b=a+1) 不起作用的原因是范围问题,而不是评估顺序问题。

函数的参数通常在调用框架中计算。当您说a+1 时,您指的是调用data.frame 的框架中的变量a,而不是您要创建的列。

dplyr::data_frame 进行非常非标准的评估,因此它可能会像您看到的那样混淆帧。它似乎首先在与正在构建的对象对应的框架中查找,然后在通常的位置查找。

dplyr 语义与基本函数一起使用的一种方法是两者都做, 例如

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 包中的代码是不可接受的。

【讨论】:

    猜你喜欢
    • 2012-08-22
    • 1970-01-01
    • 2018-11-05
    • 2011-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多