1

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)
nbenn
  • 591
  • 4
  • 12
  • Just to clarify the title edit: I think the "parent environment" nomenclature is a bit misleading here, as (to my understanding) the parent of the execution environment is the enclosing environment, which is often different from the calling environment. – Mikko Marttila Jul 19 '18 at 20:04
  • Maybe I'm misunderstanding, but it seems to me that the desired output in the additional context is somewhat contradictory to the original question? Originally you want substitution, but here you just want to capture the expression? – Mikko Marttila Jul 20 '18 at 09:02
  • 1
    My apology. I mindlessly copy-pasted for the desired output. It should be corrected now. – nbenn Jul 20 '18 at 09:07
  • 1
    So basically, you have a code snippet that you want to submit as a job; but that code snippet depends on the current context, and you have no way of passing the context to the cluster that runs the snippet? (Is that about right?) So you need to substitute in all of the information needed from the current context? – Mikko Marttila Jul 20 '18 at 09:27
  • Yep, sounds about right. – nbenn Jul 20 '18 at 09:29

3 Answers3

1

Rather than do that I suggest you just pass the environment like this:

esubstitute <- function(expr, envir) do.call("substitute", list(expr, envir))

do_something <- function(todo, envir = parent.frame()) {
  cat(
    paste(
      deparse(esubstitute(todo, envir)),
      collapse = "\n"
    )
  )
}

nested_do <- function(envir = parent.frame()) {

  var_2 <- "goodbye"

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

}

var_1 <- "hello"

nested_do()

giving:

[1] "hello"
[1] "world"
[1] "goodbye"
"goodbye"> 

You may also want to look at the envnames package.

G. Grothendieck
  • 254,981
  • 17
  • 203
  • 341
  • Thanks for giving my problem a shot. Unfortunately I don't see how this helps me. Maybe my question was not clear: What I'm trying to achieve is the following: 1) capture an expression, 2) substitute any variables bound in current or parent enviroments and 3) print the original expression with substituted variables (or write to file or else). To roughly get what you do, I can just evaluate `todo`. That works, as all variables are in scope. Or am I missing something? – nbenn Jul 19 '18 at 19:42
  • It was clear but I dont thnk it's a good idea. It would be better to try to stick to how R works as much as possible and this does that while giving the required answer to the sample code. – G. Grothendieck Jul 19 '18 at 22:36
  • If you like, could you please elaborate on why you think what I'm trying to do is a bad idea? I would very much like to "stick to how R works as much as possible", but I don't see how your solution could work for me (check out the "Additional context" section). I still think that my actual intent might not have been clear from the beginning. Don't get me wrong, I appreciate you trying to help me out. – nbenn Jul 20 '18 at 08:33
  • It will be hard to debug if anything goes wrong since it's looking everywhere. Also if you implement it via iterative substitute you can get substitution into substitutes which is really bizarre. – G. Grothendieck Jul 20 '18 at 12:28
  • So you are saying, what I want to accomplish is not possible without resorting to weird hacks, that "will be hard to debug if anything goes wrong" and end up being "really bizarre"? Sorry for twisting you words a bit ;-) Do you see my own solution breaking easily? Do you have an example where my solution falls apart? – nbenn Jul 20 '18 at 15:05
  • 1
    I was referring to writing software that uses it. I think that that software would be hard to debug since you never really know where objects are coming from. Its the same problem as using a lot of global variables. Also as stated the iterative scheme means that if an expression a is replaced with b then on the next iteration b could be replaced with c so you get multiple levels of indirection inadvertently. – G. Grothendieck Jul 20 '18 at 15:15
  • You were right. It turned out the be difficult to reliably and predictably use this. Thanks anyways. – nbenn Aug 02 '18 at 16:23
1

You can define a function to do such a substitution sequence: that is, take an expression and substitute it in all of the environments in the call stack. Here's one way:

substitute_stack <- function(expr) {
  expr <- substitute(expr)

  # Substitute in all envs in the call stack
  envs <- rev(sys.frames())
  for (e in envs) {
    expr <- substitute_q(expr, e)
  }

  # sys.frames() does not include globalenv() and
  # substitute() doesnt "substitute" there
  e <- as.list(globalenv())
  substitute_q(expr, e)
}

# A helper to substitute() in a pre-quoted expression
substitute_q <- function(expr, env = parent.frame()) {
  eval(substitute(substitute(x, env), list(x = expr)))
}

Let's give it a go:

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

nested_do <- function() {
  var_2 <- "goodbye"

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

var_1 <- "hello"

nested_do()
#> {
#>     print("hello")
#>     print("world")
#>     print("goodbye")
#> }

Whether or not actually doing this is a good idea is a whole other question. The approach suggested by @G.Grothendieck is likely to be preferrable.

Created on 2018-07-19 by the reprex package (v0.2.0.9000).

Mikko Marttila
  • 10,972
  • 18
  • 31
  • Your suggestion works for what I'm trying to do (also see the "Additional context" section). What do you not like about your solution? Why do you think the [@G.Grothendieck solution](https://stackoverflow.com/a/51429034/4550695) is preferable? – nbenn Jul 20 '18 at 08:22
  • 1
    Well, it's not that I don't like it, but it needs to be used carefully. This is essentially dynamic scoping for the expression, so you need to be very mindful of everything that's in the call stack when you use it: see e.g. [this small gist](https://gist.github.com/mikmart/b98e9d320d72eb582c438e55f695e71f). – Mikko Marttila Jul 20 '18 at 08:45
  • 1
    Thank you very much for the additional example. I was under the impression that R used dynamic scoping. However R uses lexical scoping. Thanks for helping me clear up this misconception! What I want for my substitution to apply lexical scoping as well. But this shouldn't be a problem, right? The list of environments to iterate over has to be modified, but other than that, all should be fine, no? I'll try. – nbenn Jul 20 '18 at 09:26
  • Yup. `rlang::env_parents()` may be useful there. – Mikko Marttila Jul 20 '18 at 09:32
0

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")
#> }
nbenn
  • 591
  • 4
  • 12