Environments and scoping

R · Advanced · 14 min

Understanding environments explains closures, packages, NSE and many confusing bugs.

An environment is a collection of name-value bindings plus a pointer to a parent environment. When R looks up a name, it searches the current environment, then its parent, and so on up to the global environment, the attached packages and finally the empty environment.

Each function call creates a fresh execution environment whose parent is the environment where the function was defined (not where it was called). That is lexical scoping, and it is what makes closures work: a function returned from another function keeps access to the variables of the call that created it.

  • environment(f) shows a function's enclosing environment; new.env() creates one; get, assign and exists work with names as strings.
  • <- always creates or modifies a binding in the current environment.
  • <<- walks up the parents and modifies the first existing binding it finds (or creates a global). It is the tool for stateful closures, and a code smell elsewhere.

Unlike most R objects, environments are not copied on modification: they have reference semantics. That's why they're used for caches, mutable state and as the basis of R6 and Reference Classes.

Packages have a namespace environment, so a package's functions find their own helpers even if you define an object with the same name globally.

Example

make_counter <- function() {
  count <- 0
  function() {
    count <<- count + 1      # modifies the enclosing binding
    count
  }
}
counter <- make_counter()
for (i in 1:2) counter()
print(counter())                          #> [1] 3
print(get("count", environment(counter))) #> [1] 3

x <- "global"
f <- function() x
g <- function() { x <- "local to g"; f() }
print(g())                                #> [1] "global" (lexical)

cache <- new.env()
remember <- function(env, key, val) assign(key, val, envir = env)
remember(cache, "a", 1)
print(ls(cache))                          #> [1] "a" (no copy made)
print(environmentName(parent.env(globalenv())) != "")  #> [1] TRUE

Remember

  • Name lookup walks from the current environment through its parents
  • A function's parent is where it was defined: lexical scoping
  • <<- modifies an existing binding in an enclosing environment
  • Environments have reference semantics

Watch out for

  • Using <<- inside ordinary functions to change globals, creating hidden dependencies.

Check yourself

  1. f <- function() x is defined globally and called from inside g, which has its own local x. Which x does f use?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: cran.r-project.org/doc/manuals/r-release/R-lang.html#Scope-of-variables