Home > Software engineering >  Inject Expression from One Environment and Evaluate in Another
Inject Expression from One Environment and Evaluate in Another

Time:03-20

Goals

I am developing a package, and I need a function that behaves similarly to rlang::inject() or rlang::qq_show(). This function should be of the form

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

which accepts an expression expr and injects the arguments from inj_env. Then it returns the injected expression itself, without evaluating it.

For example:

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

Unfortunately, neither inject() nor qq_show() suffice.

While inject() does have an env parameter, this is only for evaluating the expression after it has been injected. There is no inj_env from which the arguments may be injected, as they are always taken from the calling context.

Furthermore, there is no option to return the injected expression itself, as inject() will always return the result of evaluating that expression in env.

As for qq_show(), it prints the injected expression itself, but it does not return it as an object: the return value is NULL. Like inject(), it also lacks an inj_env from which arguments are injected.

Attempts

I've had some success with this approach here:

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

The idea is that when we call something like unquo_1(!!a 1, global_env()), it will create an inj_expr of inject(quote(!!a 1)). This will be evaluated in inj_env, which here includes the object a: the symbol a.global. So inject() will unquote !!a to get quote(a.global 1), which it then evaluates (also in inj_env). The result is simply the expression a.global 1.

As with my illustration of behavior in Goals, this often works as expected:

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"

Unlike a, the object inject is not defined in my_env or its environmental ancestors. If it were defined differently, as with env_bind(my_env, inject = base::stop), then its behavior would still be entirely unhelpful. The same applies for the functions quote, `!`, and so forth.


The best solution I've found is to redefine inj_expr to fully qualify rlang::inject() and base::quote():

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

On its own, this "solution" would simply produce another error

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

because `::` is unavailable in inj_env. But with inspiration from data_masking conventions

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

the simple tweak env_bind(inj_env, "::" = `::`) will make the function `::`() accessible in inj_env. This one tweak thus facilitates access via pkg::fn to any function fn in any package pkg!

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

I really do want the contents of inj_env (and its parents) restricted to exactly what the user supplies.

Suggestions

I have experimented unsuccessfully with function factories, for the sake of associating an injector with a custom environment. The documentation for rlang::env_bind_lazy() still seems promising

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

but I lack the expertise to leverage it.


Alternatively, examining the source code for rlang::inject()

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

highlights the importance of rlang::enexpr()

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

which in turn suggests that the DLL rlang:::rlang_enexpr is essential:

$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"

This seems to originate here as source code in C:

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

However, I lack the skill in C to trace how unquotation is implemented here, let alone to rewrite ffi_enexpr for my own package.

CodePudding user response:

Do you really need to store your desired symbols in an environment? It seems if you are just storing symbols/expressions then you can more easily do that in a container like exprs and then can use the with_bindings function to replace some values. So if you have

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

then you can do

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

Once you have the correct expression, you can evaluate it wherever you like.

CodePudding user response:

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
  • Related