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
不同,函数inject
是my_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
了.