Configuration:
OS : Windows 10 (64 bits)
R version: 3.6.3
I'm learning R and currently I'm reading about the environments in R. I was doing some practice and I came up with an example that I created myself, yet it seems that I'm still unable to explain and understand the concept of looking up objects in R properly. Generally speaking, what I've understood so far (please correct me if I'm wrong), is that if R doesn't find an object in the current environment it calls in order all existing parent environments. Just to see how it works in practice, I created the following program:
library(rlang)
library(envnames)
library(lobstr)
e1 <- env()
e2 <- new_environment(parent = e1)
e3 <- new_environment(parent = e2)
e4 <- new_environment(parent = e3)
e5 <- new_environment(parent = e4)
e6 <- new_environment(parent = e5)
e7 <- new_environment(parent = e6)
e8 <- new_environment(parent = e7)
e9<- new_environment(parent = e8)
e10 <- new_environment(parent = e9)
e4$testvar <- 1200
e10$testfun <- function(x) {
print(envnames::environment_name(caller_env()))
return (testvar)
}
And here is how I run the above program by selecting e10 as the caller environment
with(data = e10, expr = e10$testfun())
Given that testvar is defined in the environment e4 and e4 is an ancestor of e10, I expected that R goes up in the parents tree from e10 up to e4 in order to find the value of testvar. But the programs stops with the following error:
Error in e10$testfun() (from #3) : object 'testvar' not found
Could you tell me what I've misunderstood? The fact that I use with(data = e10, ...)
shouldn't imply that the environment used for the function call would be e10?
So, this is an unusually nuanced issue. There are two relevant types of environments that you need to think about here, the binding environment, or the environment that has a binding to your function, and the enclosing environment, or the environment where your function was created. In this case the binding environment is e10
, but the enclosing environment is the global environment. From Hadley Wickham's Advanced R:
The enclosing environment belongs to the function, and never changes, even if the function is moved to a different environment. The enclosing environment determines how the function finds values; the binding environments determine how we find the function.
Consider the following (executed after executing your supplied code) that demonstrates this:
eval(expression(testfun()), envir = e10)
# [1] "e10"
# Error in testfun() : object 'testvar' not found
testvar <- 600
eval(expression(testfun()), envir = e10)
# [1] "e10"
# [1] 600
Moreover, now consider:
eval(envir = e10, expr = expression(
testfun2 <- function(x) {
print(envnames::environment_name(caller_env()))
return (testvar)
}
))
eval(expression(testfun2()), envir = e10)
# [1] "e10"
# [1] 1200
I hope this clarifies the issue.
So how can we determine the binding and enclosing environments for functions such as testfun()
?
As G. Grothendieck's answer shows, the environment()
function gives you the enclosing environment for a function:
environment(e10$testfun)
# <environment: R_GlobalEnv>
To my knowledge, there isn't a simple function in base R to give you a function's binding environments. If the function you're looking for is in a parent environment, you can use pryr::where()
:
pryr::where("mean")
# <environment: base>
(There is a base
function to see if a function is in an environment, exists()
, and pryr::where()
uses it. But, it doesn't recurse through parent environments like where()
.)
However, if you're having to search through child environments, to my knowledge there isn't a function for that. But, seems pretty simple to mock one up:
get_binding_environments <- function(fname) {
## First we need to find all the child environments to search through.
## We don't want to start from the execution environment;
## we probably want to start from the calling environment,
## but you may want to change this to the global environment.
e <- parent.frame()
## We want to get all of the environments we've created
objects <- mget(ls(envir = e), envir = e)
environments <- objects[sapply(objects, is.environment)]
## Then we use exists() to see if the function has a binding in any of them
contains_f <- sapply(environments, function(E) exists(fname, where = E))
return(unique(environments[contains_f]))
}
get_binding_environments("testfun")
# [[1]]
# <environment: 0x55f865406518>
e10
# <environment: 0x55f865406518>