Update

事实证明,函数rlang::expr_interp()基本上满足了我的目标.

unquo_2 <- function(expr, inj_env = rlang::caller_env(), eval_env = NULL) {
  # Capture verbatim the argument passed to 'expr'...
  expr_quo <- rrlang::enquo0(expr)
  # ...and extract it literally as an expression.
  expr_lit <- rlang::quo_get_expr(expr_quo)
  
  # Unquote that expression in the context of the injection environment.
  expr_inj <- rlang::expr_interp(expr_lit, inj_env)
  
  # As desired, return either the unquoted expression itself...
  if(rlang::is_null(eval_env)) {
    expr_inj
  }
  # ...or the result of evaluating it in the evaluation environment.
  else {
    rlang::eval_bare(expr_inj, eval_env)
  }
}

不幸的是,expr_interp()deprecated,赞成inject()inject()既不适应单独的注入环境,也不在计算之前返回表达式.


Goals

我正在开发一个包,我需要一个行为类似于rlang::inject()rlang::qq_show()的函数.此函数的形式应为

unquo <- function(expr, inj_env) {
  # ...
}

它接受表达式expr,并从inj_env中注入参数.然后返回注入的表达式本身,并对其进行计算.

例如:

library(rlang)



# Arguments to inject from global environment.
a <- sym("a.global")
b <- sym("b.global")


# Arguments to inject from custom environment, which has no parent.
my_env <- new_environment(list(a = sym("a.custom")))



# Injecting from global environment.
unquo(!!a + !!b, global_env())
#> a.global + b.global


# Injecting from custom environment...
unquo(!!a + 1, my_env)
#> a.custom + 1

# ...where 'b' neither exists nor is inherited.
unquo(!!a + !!b, my_env)
#> Error in enexpr(expr) : object 'b' not found

Roadblock

不幸的是,inject()qq_show()都不够.

虽然inject()确实有一个env参数,但这仅适用于evaluating它已注入的表达式after.由于参数总是从调用上下文中获取,因此没有可注入的参数.

此外,没有返回注入表达式itself的选项,因为inject()将始终在env中返回evaluating该表达式的结果.

至于qq_show(),它将插入表达式本身,但不将其作为对象:返回值为NULL.和inject()一样,它也缺少一个inj_env,可以从中注入参数.

Attempts

我在这方面取得了一些成功:

unquo_1 <- function(expr, inj_env) {
  inj_expr <- substitute(inject(quote(expr)))
  eval_bare(inj_expr, inj_env)
}

我们的 idea 是,当我们称之为unquo_1(!!a + 1, global_env())时,它将产生inject(quote(!!a + 1))中的inj_expr.这将在inj_env中进行判断,其中包括对象a:符号a.global.所以inject()将取消!!a的报价,得到quote(a.global + 1),然后对其进行判断(同样在inj_env中).结果就是表达式a.global + 1.

正如我在《Goals》中对行为的描述一样,这通常会如预期的那样起作用:

unquo_1(!!a + 1, global_env())
#> a.global + 1

unquo_1(!!a + !!b, global_env())
#> a.global + b.global

unquo_1(!!a + !!z, global_env())
#> Error in enexpr(expr) : object 'z' not found

However, there is a subtle yet critical edge case, which defeats the entire purpose:

unquo_1(!!a + 1, my_env)
#> Error in inject(quote(!!a + 1)) : could not find function "inject"

a不同,函数injectmy_env中的对象undefined及其环境祖先.如果它的定义与env_bind(my_env, inject = base::stop)不同,那么它的行为仍然毫无帮助.这同样适用于功能quote`!`等.


我找到的最佳解决方案是重新定义inj_expr以完全限定rlang::inject()base::quote():

unquo_1 <- function(expr, inj_env) {
  inj_expr <- substitute(rlang::inject(base::quote(expr)))
  eval_bare(inj_expr, inj_env)
}

这个"解决方案"本身只会产生另一个错误

Error in rlang::inject : could not find function "::"

因为`::`inj_env中不可用.但灵感来自于data_mask场大会

 # A common situation where you'll want a multiple-environment mask
 # is when you include functions in your mask. In that case you'll
 # put functions in the top environment and data in the bottom. This
 # will prevent the data from overwriting the functions.
 top <- new_environment(list(`+` = base::paste, c = base::paste))

简单的调整env_bind(inj_env, "::" = `::`)将使功能`::`()inj_env中可访问.因此,这一调整有助于通过pkg::fn访问任何软件包pkg中的任何功能fn

However, this still exposes unquo_1() to naming collisions. What if someone wanted to inject the expression !!`::` with an alternative function named ::?.

我真的希望inj_env(及其父母)的内容仅限于用户提供的exactly.

Suggestions

为了将喷油器与自定义环境相关联,我用function factories进行了试验,但没有成功.rlang::env_bind_lazy()的文档似乎仍有希望

 # By default the expressions are evaluated in the current
 # environment. For instance we can create a local binding and refer
 # to it, even though the variable is bound in a different
 # environment:
 who <- "mickey"
 env_bind_lazy(env, name = paste(who, "mouse"))
 env$name
 #> [1] "mickey mouse"
 
 # You can specify another evaluation environment with `.eval_env`:
 eval_env <- env(who = "minnie")
 env_bind_lazy(env, name = paste(who, "mouse"), .eval_env = eval_env)
 env$name
 #> [1] "minnie mouse"

但我缺乏利用它的专业知识.


或者,判断源代码rlang::inject()分钟

function (expr, env = caller_env()) 
{
    .External2(rlang_ext2_eval, enexpr(expr), env)
}

强调rlang::enexpr()的重要性

function (arg) 
{
    .Call(rlang_enexpr, substitute(arg), parent.frame())
}

这反过来表明DLL rlang:::rlang_enexpr是必不可少的:

$name
[1] "rlang_enexpr"

$address
<pointer: 0x7ff630452a60>
attr(,"class")
[1] "RegisteredNativeSymbol"

$dll
DLL name: rlang
Filename:
         /Library/Frameworks/R.framework/Versions/4.1/Resources/library/rlang/libs/rlang.so
Dynamic lookup: FALSE

$numParameters
[1] 2

attr(,"class")
[1] "CallRoutine"      "NativeSymbolInfo"

这似乎源于C语言中的源代码:

r_obj* ffi_enexpr(r_obj* sym, r_obj* frame) {
  return capture(sym, frame, NULL);
}

然而,我缺乏C语言的技巧来跟踪Unquote是如何在这里实现的,更不用说为我自己的包重写ffi_enexpr了.

推荐答案

Original

a <- sym("a.global")
b <- sym("b.global")

# Note this must be an environment that inherits from
# base. `new_environment()` creates envs that inherit from the empty
# env by default, which means even `::` is not in scope.
# Here we use `env()` which inherits from the current env by default.
my_env <- env(a = sym("a.custom"))

expr2 <- function(expr, env = caller_env()) {
  # Grab the defused expression using base R to avoid processing
  # rlang injection operators
  expr <- substitute(expr)

  # Inject the expression within `expr()` so it can process the
  # operators within `env`. Qualify with `::` because `env`
  # potentially doesn't have `expr()` in scope.
  inject(rlang::expr(!!expr), env)
}

expr2(!!a + !!b, my_env)
#> a.custom + b.global

expr2(!!a + !!b)
#> a.global + b.global

Update

您可以在调用中输入inline expr,正如在GitHub上实现的here:

expr2 <- function(expr, env = caller_env()) {
  # Grab the defused expression using base R to avoid processing
  # rlang injection operators
  expr <- substitute(expr)

  # Inject the expression within `expr()` so it can process the
  # operators within `env`. Inline `expr` in case it isn't in scope
  inject((!!rlang::expr)(!!expr), env)
}

R相关问答推荐

创建重复删除的唯一数据集组合列表

按R中的组查找相邻列的行累积和的最大值

编辑文件后编辑RhandsonTable

如何求解arg必须为NULL或deSolve包的ode函数中的字符向量错误

用黄土法确定区间

如何写一个R函数来旋转最后n分钟?

lightgbm发动机在tidymmodels中的L1正则化""

在使用tidyModels和XGBoost的二进制分类机器学习任务中,所有模型都失败

`lazy_dt`不支持`dplyr/across`?

用两种 colored颜色 填充方框图

跨列查找多个时间报告

是否有新方法来更改Facet_WRAP(Ggplot2)中条文本的文本 colored颜色 ?

使用shiny 中的所选要素行下拉菜单

为什么在写入CSV文件时Purrr::Pwalk不起作用

仅当后续值与特定值匹配时,才在列中回填Nas

有毒元素与表观遗传年龄的回归模型

注释不会绘制在所有ggplot2面上

从多行中 Select 最小值

基于已有ID列创建唯一ID

按顺序将地块添加到列表