Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

evaluate call when components may be scattered among environments

Tags:

r

eval

I have an expression expr that I want to evaluate; the symbol/value pairs I need to evaluate it may be in one (or more!) of three environments, and I'm not sure which. I'd like to find a convenient, efficient way to "chain" the environments. Is there a way to do this safely while avoiding the copying of contents of environments?

Here's the setup:

env1 <- list2env(list(a=1))
env2 <- list2env(list(b=1))
env3 <- list2env(list(c=1))
expr <- quote(a+b)

So, I will need to evaluate expr in the combination of environments env1 and env2 (but I don't necessarily know that). Any of eval(expr, env1); eval(expr, env2); and eval(expr,env3) will fail, because none of those environments contains all of the required symbols.

Let's suppose I'm willing to assume that the symbols are either in env1+env2 or in env1+env3. I could:

  1. Create combined environments for each of those pairs as described in this question.

problems:

  • if I use one of the solutions that involves creating new environments, and one of my environments has a lot of stuff in it, this could be expensive.
  • using parent.env()<- could be a bad idea — as described in ?parent.env:

The replacement function parent.env<- is extremely dangerous as it can be used to destructively change environments in ways that violate assumptions made by the internal C code. It may be removed in the near future.

(although, according the source history, that warning about removal "in the near future" is at least 19 years old ...)

(in fact I've already managed to induce some infinite loops playing with this approach)

  1. use
tryCatch(eval(call, envir=as.list(expr1), enclos=expr2),
         error=function(e) {
             tryCatch(eval(call, as.list(expr1), enclos=expr3))

to create an "environment within an environment"; try out the combined pairs one at a time to see which one works. Note that enclos= only works when envir is a list or pairlist, which is why I have to use as.list().

problem: I think I still end up copying the contents of expr1 into a new environment.

I could use an even more deeply nested set of tryCatch() clauses to try out the environments one at a time before I resort to copying them, which would help avoid copying where unnecessary (but seems clunky).

like image 879
Ben Bolker Avatar asked Oct 08 '20 18:10

Ben Bolker


2 Answers

Convert the enviroments to lists, concatenate them and use that as the second arg of eval. Note that this does not modify the environments themselves.

L <- do.call("c", lapply(list(env1, env2, env3), as.list))
eval(expr, L)
## [1] 2

Also note that this does not copy the contents of a, b and c. They are still at the original addresses:

library(pryr)

with(env1, address(a))
## [1] "0x2029f810"

with(L, address(a))
## [1] "0x2029f810"
like image 92
G. Grothendieck Avatar answered Sep 30 '22 04:09

G. Grothendieck


No, there's no simple way to chain environments. As you know, every environment has a parent which is another environment, so overall environments form a tree structure. (The root of the tree is the empty environment.) You can't easily take a leaf from a tree and graft it onto another leaf without making structural changes to it.

So if you really need to evaluate your expression in the way you describe, you're going to have to parse it, look up the names yourself, and substitute values into it. But even this isn't necessarily going to give you the same value at the end, because substitute() and similar functions might be involved in it.

My advice would be to start over, and don't try to make an expression like the one you're talking about. This might involve copying, but remember that copying is usually cheap in R: the cost only comes if you modify one of the copies.

Edited to add:

The current other four answers are implicitly making assumptions that env1 to env3 are as simple as they are in your example. If that's true, then I'd go with @G.Grothendieck's solution. But all fail in this simple variation on your example:

env1 <- list2env(list(a=1))
env2parent <- list2env(list(b=1))
env2 <- new.env(parent = env2parent)
env3 <- list2env(list(c=1))
expr <- quote(a+b)

I can evaluate quote(b) using eval(quote(b), envir = env2), but I can't evaluate expr using the other solutions unless I also include env2parent in the list of environments being passed.

Edited again:

Here's a solution that essentially does what I suggested, except instead of parsing, it uses the all.vars function from one of @r2evans answers. It works by copying all the variables into a common environment, so copying happens, but the names are kept:

envfunc3 <- function(expr, ...) {
  vars <- all.vars(expr)
  env <- new.env()
  for (v in vars) {
    for (e in list(...))
      if (exists(v, envir = e)) {
        assign(v, get(v, envir = e), envir = env)
        break
      }
  }
  eval(expr, envir=env)
}
like image 37
user2554330 Avatar answered Sep 30 '22 06:09

user2554330