1

In R, running the expression x <- 1 defines a variable x in the global environment with the value 1. Doing the same within a function defines the variable within the function's environment instead.

Using rlang::with_env, we can also do the same thing with an arbitrary environment:

e <- new.env()

rlang::with_env(e, {
  x <- 1
  y <- 2
  f <- function(x) print(x)
  g <- function() f(1)
})

e$x
#> [1] 1
e$g()
#> [1] 1

Created on 2021-10-26 by the reprex package (v2.0.1)

However, I can't figure out how to do the same in a function. That is, a function which receives expressions and then runs them in a blank environment, returning the environment:

set_in_env <- function(expr) {
  e <- new.env()
  
  # q <- rlang::enquo(expr)
  # z <- quote(expr)
  
  # rlang::with_env(e, substitute(expr))
  # rlang::with_env(e, parse(text = substitute(expr)))
  # rlang::with_env(e, q)
  # rlang::with_env(e, rlang::eval_tidy(q))
  # rlang::with_env(e, z)
  # rlang::with_env(e, eval(z))
  rlang::with_env(e, expr)
  rlang::with_env(e, {x <- 1})
  
  return(e)
}

e <- set_in_env({y <- 2})
  
rlang::env_print(e)
#> <environment: 0000000014678340>
#> parent: <environment: 0000000014678730>
#> bindings:
#>  * x: <dbl>          <-- ONLY `x` WAS SET, NOT `y`!

That is, the function is given the expression y <- 2 which should be run in a new environment. For demonstration purposes, the function also internally sets x <- 1 in the environment.

No matter what I've tried, the environment is only created with e$x, never defining e$y <- 2 (the commented out code were other failed attempts).

I'm confident this can be done and that I'm just missing something. So, can someone give me a hand?

Wasabi
  • 2,879
  • 3
  • 26
  • 48
  • Try inserting `cat(y)`, just before the `return()` statement in your `set_in_env()`. If you avoid an `object not found` error for `y`, it means the expression `{y <- 2}` was first evaluated within `set_in_env()` before being passed to `rlang::with_env()` as the value for its `expr` argument. – Greg Oct 26 '21 at 21:15

4 Answers4

1

It's odd that the with_env function doesn't seem to allow for injecting expressions into the expression parameter. Here's a work around

set_in_env <- function(expr) {
  
  e <- new.env()
  q <- rlang::enexpr(expr)

  rlang::inject(rlang::with_env(e, !!q))
  rlang::with_env(e, {x <- 1})
  
  return(e)
}

We explicltly use rlang::inject to inject the expression to the call and then inject will also evaluate it.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • Thanks for the solution. I'd thought of building the call myself in this way using `call2` and `call_modify` (wasn't aware of `inject`), but that felt really clunky and non-idiomatic, glad to know you agree by calling this a workaround. I've posted [an issue on the `rlang` GitHub](https://github.com/r-lib/rlang/issues/1306) to see if this is a feature or a bug. – Wasabi Oct 26 '21 at 20:31
1

This could be a base solution:

set_in_env <- function(expr) {
    e <- new.env()
    
    # Resolve the given 'expr'ession as a 'call', before evaluating that call in the
    # environment 'e'.  Otherwise, 'expr' will first be evaluated within 'set_in_env()',
    # with such consequences as described below.
    eval(expr = substitute(expr), envir = e)
#   ^^^^        ^^^^^^^^^^
# 'eval()' with 'substitute()'

    # Avoid evaluating anything whatsoever about the 'x <- 1' assignment, until doing so
    # in the environment 'e'.  Otherwise, 'x <- 1' will first be evaluated within 
    # 'set_in_env()', and 'x' will be available in 'set_in_env()' yet unavailable in the
    # environment 'e'.
    evalq(expr = {x <- 1}, envir = e)
#   ^^^^^
# 'evalq()' on its own
    
    return(e)
}

When we put set_in_env() through its paces as in your question

e <- set_in_env({y <- 2})
  
rlang::env_print(e)

we get the desired results:

<environment: 0000013E34E1E0D0>
parent: <environment: 0000013E34E1E488>
bindings:
 * x: <dbl>
 * y: <dbl>
Greg
  • 3,054
  • 6
  • 27
0

1) We can use eval/substitute like this:

f <- function(expr) {
  eval(substitute({
     expr
     x <- 1
  }), e <- new.env())
  e
}

# test
e <- f( {y <- 2} )
ls(e)
## [1] "x" "y"

2) or we can reuse the environment/frame within f like this:

f <- function(expr) {
  eval(substitute({
     expr
     x <- 1
   }))
   rm(expr)
   environment()
}

e <- f( {y <- 2} )
ls(e)
## [1] "x" "y"
G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
0

I raised this as an issue in the rlang GitHub, where among other comments (including that he intends to deprecate with_env) @lionel gave a very clean way of doing this:

library(rlang)

set_in_env <- function(expr) {
  e <- env()
  
  expr <- rlang::enexpr(expr)
  
  rlang::eval_bare(expr, e)
  
  return(e)
}

e <- set_in_env({y <- 2})
e$y
#> [1] 2

Created on 2021-10-27 by the reprex package (v2.0.1)

I'd actually tried eval_tidy with quosures, what I needed was eval_bare with expressions.

Wasabi
  • 2,879
  • 3
  • 26
  • 48