从一个环境注入表达式并在另一个环境中评估

Inject Expression from One Environment and Evaluate in Another

更新

事实证明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 in favor of inject(),它既不适合单独的注入环境,也不适合在评估之前 return 表达式。


目标

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

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

接受表达式 expr 并注入来自 inj_env 的参数。然后它 return 是注入的表达式本身, 没有 评估它。

例如:

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

障碍

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

虽然 inject() 确实有一个 env 参数,但这仅用于 评估 表达式 它之后已注射。没有 inj_env from 可以注入参数,因为它们总是从调用上下文中获取。

此外,没有选择 return 注入的表达式 本身 ,因为 inject() 总是 return [= 的结果156=]正在评估 env.

中的表达式

至于qq_show(),它打印注入的表达式本身,但它不return它作为对象:return 值为 NULL。与 inject() 一样,它也缺少用于注入参数的 inj_env

尝试次数

我在这里使用这种方法取得了一些成功:

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

这个想法是,当我们调用类似 unquo_1(!!a + 1, global_env()) 的东西时,它会创建一个 inj_exprinject(quote(!!a + 1))。这将在 inj_env 中计算,这里包括对象 a:符号 a.global。因此 inject() 将取消对 !!a 的引用以得到 quote(a.global + 1),然后对其求值(也在 inj_env 中)。结果只是表达式 a.global + 1.

正如我在 目标 中对行为的说明一样,这通常会按预期工作:

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

但是,有一个微妙但关键的边缘情况,它破坏了整个目的:

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

a不同,函数injectmy_env及其环境祖先中的一个未定义对象。如果它的定义不同,如 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_masking 惯例中得到启发

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

可在 inj_env 中访问的简单调整 env_bind(inj_env, "::" = `::`) will make the function `::`()。因此,这一调整有助于通过 pkg::fn 访问任何包 pkg!

中的任何函数 fn

然而,这仍然会使 unquo_1() 暴露于 命名冲突 。如果有人想用一个名为 :: 的替代函数注入表达式 !!`::` 怎么办?

我确实希望 inj_env(及其父级)的内容完全用户提供的内容。

建议

我尝试过 function factories, for the sake of associating an injector with a custom environment. The documentation for 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"

这似乎起源于 here 作为 C:

中的源代码
r_obj* ffi_enexpr(r_obj* sym, r_obj* frame) {
  return capture(sym, frame, NULL);
}

然而,我缺乏 C 语言的技能来跟踪这里的取消引用是如何实现的,更不用说为我自己的包重写 ffi_enexpr

您真的需要在环境中存储您想要的符号吗?似乎如果你只是存储 symbols/expressions 那么你可以更容易地在像 exprs 这样的容器中做到这一点,然后可以使用 with_bindings 函数来替换一些值。所以如果你有

a <- expr(a.global)
b <- expr(b.global)
my_env <- exprs(a = a.custom)

那你就可以了

with_bindings(expr(!!a + 1))
# a.global + 1
with_bindings(expr(!!a + 1), !!!my_env)
# a.custom + 1
with_bindings(expr(!!a + !!z), !!!my_env)
# Error in enexpr(expr) : object 'z' not found

一旦你有了正确的表达式,你就可以在任何你喜欢的地方计算它。

原创

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

更新

您可以 in the call, as implemented here 在 GitHub:

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)
}