Search code examples
rscopesubstitutionrlangnse

R: Substitute variables bound in all parent environments


The function base::substitute(expr, env), as per its documentation page,

returns the parse tree for the (unevaluated) expression expr, substituting any variables bound in env.

I am looking for a way of substituting any variables bound not in one specific environment, but in all environments in the current call stack, i.e. all environments encountered by iterating over parent.frame(i) where i is in seq_len(sys.nframe()). Additionally, I'd like standard scoping rules to apply.

This is a contradiction: standard scoping in R is lexical, but what I describe here is dynamic scoping (thank you @MikkoMarttila for helping me clear this up). What I actually want is a way of substituting any variables bound not in one specific environment, but in all parent enclosing environments, the set of which can be enumerated by repeatedly applying base::parent.env().

Consider the following example:

do_something <- function(todo) {
  cat(
    paste(
      deparse(substitute(todo, environment())),
      collapse = "\n"
    )
  )
}

nested_do <- function() {

  var_2 <- "goodbye"

  do_something({
    print(var_1)
    print("world")
    print(var_2)
  })

}

var_1 <- "hello"

nested_do()

Currently this gives

print(var_1)
print("world")
print(var_2)

where I'd like to have

print("hello")
print("world")
print("goodbye")

I have looked at base::bquote() and rlang::enexpr() but for both I have to explicitly mark the variables for substitution/unquoting with .() or !!. I'd rather not have to specify variables manually, but have everything resolved that is found (just like in base::substitute()). Furthermore, I tried iteratively applying base::substitute() with the respective env arguments and I had a look at oshka::expand(), but nothing I tried, does what I need.

Any help is much appreciated.

Additional context

What I'm trying to achieve is the following: I'm working on a cluster running LSF. This means that I can submit jobs using the submission tool bsub which may take an R file as input. Now I would like to have a script that generates these input files (e.g. using the function do_something()).

long_running_fun <- function(x) {
  Sys.sleep(100)
  x / 2
}

var_1 <- 2 + 2
var_2 <- var_1 + 10

do_something({
  print(var_1)
  var_3 <- long_running_fun(var_2)
  print(var_3)
})

I in the above case, want the following (or something equivalent) to be written to a file

print(4)
var_3 <- long_running_fun(14)
print(var_3)

Solution

  • Building on @MikkoMarttila's answer, I think the following does what I requested

    do_something <- function(todo) {
    
      # A helper to substitute() in a pre-quoted expression
      substitute_q <- function(expr, env) {
        eval(substitute(substitute(x, env), list(x = expr)))
      }
    
      substitute_parents <- function(expr) {
        expr <- substitute(expr)
    
        # list all parent envs
        envs <- list()
        env <- environment()
        while (!identical(env, globalenv())) {
          envs <- c(envs, env)
          env <- parent.env(env)
        }
        # substitute in all parent envs
        for (e in envs) {
          expr <- substitute_q(expr, e)
        }
    
        # previously did not include globalenv() and
        # substitute() doesnt "substitute" there
        e <- as.list(globalenv())
        substitute_q(expr, e)
      }
    
      cat(
        paste(
          deparse(substitute_parents(todo)),
          collapse = "\n"
        )
      )
    }
    

    This gives

    nested_do <- function() {
      var_2 <- "not_this"
    
      do_something({
        print(var_1)
        Sys.sleep(100)
        print("world")
        print(var_2)
      })
    }
    
    var_1 <- "hello"
    var_2 <- "goodbye"
    
    do_something({
      print(var_1)
      Sys.sleep(100)
      print("world")
      print(var_2)
    })
    #> {
    #>     print("hello")
    #>     Sys.sleep(100)
    #>     print("world")
    #>     print("goodbye")
    #> }
    nested_do()
    #> {
    #>     print("hello")
    #>     Sys.sleep(100)
    #>     print("world")
    #>     print("goodbye")
    #> }