Environments

Applying R to Lifestyle and Brain Health Research

Ian Weidling, PhD

University of Kansas Medical Center

September 16, 2026

Environment Basics

An environment is similar to a named list with four important exceptions:

  • Every name must be unique
  • The names in an environment are not ordered
  • An environment has a parent
  • Environments are not copied when modified

We can use rlang::env() to create an environment.

e1 <- rlang::env(
  a = FALSE,
  b = "a",
  c = 2:3,
  d = 1:3
)

An environment’s job is to bind a set of names to their values. The names in the environment are in no particular order.

Environments are modified in place and do not create a copy. Unlike other R objects, environments can also contain themselves (e.g., e1$d <- e1)

Printing an environment displays its memory address, but env_print can provide more useful information. We can also use names to retrieve a character vector of the names in the environment.

e1
#> <environment: 0x10c609af8>

rlang::env_print(e1)
#> Parent: <environment: global>
#> Bindings:
#> • a: <lgl>
#> • b: <chr>
#> • c: <int>
#> • d: <int>

names(e1)
#> [1] "a" "b" "c" "d"

Current and Global Environments

Two of the most fundamental environments are the current and global environment. The current environment is where your code is executing and the global environment is your workspace where interactive computation outside of a function takes place.

# Access your current environment
rlang::current_env()
#> <environment: R_GlobalEnv>

# Access your global environment
rlang::global_env()
#> <environment: R_GlobalEnv>

# Check if the current and global environments are the same
identical(rlang::current_env(), rlang::global_env())
#> [1] TRUE

Parent Environments

Every environment has a parent environment. This is used to implement lexical scoping, allowing R to look for values in the parent environment if they are not found in the current one.

You can set a parent environment by providing an unnamed argument to the env() function and identify the parent of an environment with env_parent.

e2a <- rlang::env(d = 4, e = 5)
e2b <- rlang::env(e2a, a = 1, b = 2, c = 3)

rlang::env_parent(e2b)
#> <environment: 0x11b0e5ac0>

identical(rlang::env_parent(e2b), e2a)
#> [1] TRUE

rlang::env_parent(e2a)
#> <environment: R_GlobalEnv>

The only environment that does not have a parent is the empty environment. The ancestors of every environment eventually terminate with the empty environment.

e2c <- rlang::env(rlang::empty_env(), d = 4, e = 5)
e2d <- rlang::env(e2c, a = 1, b = 2, c = 3)

rlang::env_parents(e2b)
#> [[1]]   <env: 0x13dcc5250>
#> [[2]] $ <env: global>

rlang::env_parents(e2d)
#> [[1]]   <env: 0x13dd654f0>
#> [[2]] $ <env: empty>

By default, env_parents will stop at the global environment as the ancestors of the global environment include all attached packages.

rlang::env_parents(e2b, last = rlang::empty_env())

#>  [[1]]   <env: 0x113640f70>
#>  [[2]] $ <env: global>
#>  [[3]] $ <env: tools:rstudio>
#>  [[4]] $ <env: tools:positron>
#>  [[5]] $ <env: package:stats>
#>  [[6]] $ <env: package:graphics>
#>  [[7]] $ <env: package:grDevices>
#>  [[8]] $ <env: package:utils>
#>  [[9]] $ <env: package:datasets>
#> [[10]] $ <env: package:methods>
#> [[11]] $ <env: Autoloads>
#> [[12]] $ <env: package:base>
#> [[13]] $ <env: empty>

Super assignment

Regular assignment (<-) creates a variable in the current environment. Super assignment (<<-) modifies an existing variable found in a parent environment.

x <- 0
f <- function() {
  x <<- 1
}
f()
x
#> [1] 1

If <<- does not find an existing variable in the parent environments, it will create one in the global environment.

Getting and Setting

You can use the subsetting operators $ and [[ to get and set elements of an environment. However, you can not pass numeric indices to [[ as the names are unordered.

e3 <- rlang::env(x = 1, y = 2)
e3$x
#> [1] 1

e3$z <- 3
e3[["z"]]
#> [1] 3

e3[[1]]
#> Error in `e3[[1]]`: ! wrong arguments for subsetting an environment

A value of NULL is returned if the binding does not exisit, but env_get can be used to return an error or a default value.

e3$xyz
#> NULL

rlang::env_get(e3, "xyz")
#> Error in `rlang::env_get()`: ! Can't find `xyz` in environment.

rlang::env_get(e3, "xyz", default = NA)
#> [1] NA

You can also add bindings to an environment with env_poke and env_bind.

  • env_poke takes a name and a value
  • env_bind allows you to bind multiple values
rlang::env_poke(e3, "a", 100)

e3$a
#>[1] 100

rlang::env_bind(e3, a = 10, b = 20)

names(e3)
#> [1] "x" "y" "z" "a" "b"

You can determine whether a binding exisits with env_has and unbind a name from an environment with env_unbind.

rlang::env_has(e3, "a")
#>   a
#> TRUE

rlang::env_unbind(e3, "a")
rlang::env_has(e3, "a")
#>   a
#> FALSE

Advanced Bindings

The env_bind_lazy function creates delayed bindings to be evaluated the first time they are accessed. It’s primary use is in autoload to allow R packages to provide datasets that behave like they are loaded in memory (e.g., dplyr::starwars).

rlang::env_bind_lazy(
  rlang::current_env(),
  b = {
    Sys.sleep(1)
    1
  }
)

system.time(print(b))
#> [1] 1
#>    user  system elapsed
#>   0.001   0.002   1.002

system.time(print(b))
#> [1] 1
#>    user  system elapsed
#>       0       0       0

The env_bind_active function creates active bindings to be recomputed each time they are accessed. These bindings are used to implement the active fields of R6 functions.

rlang::env_bind_active(rlang::current_env(), z1 = function(val) runif(1))

z1
#> [1] 0.7908209

z1
#> [1] 0.6752419

Recursing over environments

Operating on every ancestor of an environment is possible with recursive functions. For example, writing a function like where can be used to find the environment of a given name using R’s scoping rules.

where <- function(name, env = rlang::caller_env()) {
  if (identical(env, rlang::empty_env())) {
    stop("Can't find ", name, call. = FALSE)
  } else if (rlang::env_has(env, name)) {
    env
  } else {
    where(name, rlang::env_parent(env))
  }
}

# The base case
where("yyy")
#> Error: Can't find yyy

# The successful case
x <- 5
where("x")
#> <environment: R_GlobalEnv>

# The recursive case
where("mean")
#> <environment: base>

Iteration Instead of Recursion

The same where function can be rewritten with a while loop. We don’t often use recursion, so it may be easier to understand through iteration.

where <- function(name, env = rlang::caller_env()) {
  while (!identical(env, rlang::empty_env())) {
    if (rlang::env_has(env, name)) {
      return(env)
    } else {
      env <- rlang::env_parent(env)
    }
  }
}

e4a <- rlang::env(rlang::empty_env(), a = 1, b = 2)

e4b <- rlang::env(e4a, x = 10, a = 11)

# Finds a in environment e4b
identical(where("a", e4b), e4b)
#> [1] TRUE

# Finds b in parent environment e4a
identical(where("b", e4b), e4a)
#> [1] TRUE

Package Environment

Each package becomes a parent of the global environment when attached with library or require. The last package attached is the first parent of the global environment.

The search path allows you to see the order in which every package was attached. You can use base::search or rlang::search_envs to see the list of packages.

base::search()

#>  [1] ".GlobalEnv"        "tools:rstudio"     "tools:positron"
#>  [4] "package:stats"     "package:graphics"  "package:grDevices"
#>  [7] "package:utils"     "package:datasets"  "package:methods"
#>  [10] "Autoloads"        "package:base"

The last two environments on the search path are always the same:

  • Autoloads: Uses delayed bindings to save memory by only loading objects when needed
  • Base: Environment of the base package

When you attach another package, the parent of the global environment changes.

Function Environment

A function binds the current environment when it is created (called a function environment). Across other programming languages, functions that capture (or enclose) their environments are called closures.

You can get the function environment with base::environment or rlang::fn_env.

y <- 1

f <- function(x) x + y

base::environment(f)
#> <environment: R_GlobalEnv>

The function f() binds the environment that binds the name f to the function.

However, this isn’t always the case. Some times a name is bound to a new environment but the function binds the global environment.

e <- rlang::env()
e$g <- function() 1

The difference between binding and being bound is important as it changes how we find the name of the function vs how it finds its variables.

Namespaces

The goal of namespaces is to ensure that packages find their functions and that packages work the same regardless of what other packages are attached. R avoids this problem by associating every function in a package with a package and namespace environment.

  • The package environment is the external interface to the package and how you find functions (e.g., ::).
  • The namespace environment is the internal interface to the package and controls how the function finds its variables.

Every binding in the package environment is also found in the namespace environment to ensure that all functions can use every other function in the package. But some bindings only occur in the namespace environment (i.e., internal or non-exported objects.), allowing the ability to hide internal implementation details.

Every namespace has an imports environment that contains bindings to all the functions used by the package.

  • The parent of the imports environment is the base namespace to avoid the need to import all of the base functions
  • The parent of the base namespace is the global environment

Execution environments

A new environment is created to host execution every time a function is called. This is the execution environment and its parent is the function environment.

Take this function as an example:

h <- function(x) {
  # 1.
  a <- 2 # 2.
  x + a
}

y <- h(1) # 3.

The execution environment is garbage collected once the function completes.

There are a few ways to make the function environment stay around. First, you can explicitly return the environment.

h2 <- function(x) {
  a <- x * 2
  rlang::current_env()
}

e <- h2(x = 10)

rlang::env_print(e)

#> <environment: 0x13278ad98>
#> Parent: <environment: global>
#> Bindings:
#> • a: <dbl>
#> • x: <dbl>

Second, you can return an object with a binding to that environment. The following example uses a function factory (i.e., a function that makes functions) to illustrate that idea.

plus <- function(x) {
  function(y) x + y
}

plus_one <- plus(1)

plus_one

#> function(y) x + y
#> <environment: 0x134575ba0>

plus_one(2)
#> [1] 3

The enclosing environment of plus_one is the execution environment of plus in this function factory.

When plus_one is run, its execution environment captures the execution environment of plus.

Call Stacks

The caller environment can be accessed with rlang::caller_env or base::parent.frame and provides the environment from which the function was called. It can be different based on how the function is called.

Executing a function creates two types of context:

  • The execution environment is a child of the function environment and determined by where the function was created.
  • The call stack is made up of frames and created by where the function was called.

Simple Call Stack

The most common way to see a call stack in R is by looking at the traceback after an error has occurred.

f <- function(x) {
  g(x = 2)
}

g <- function(x) {
  h(x = 3)
}

h <- function(x) {
  stop()
}

f(x = 1)
#> Error in `h()`

traceback()
#> 4: stop() at #2
#> 3: h(x = 3) at #2
#> 2: g(x = 2) at #2
#> 1: f(x = 1)

We can also use lobstr::cst to print out the call stack tree. It prints the call stack from the beginning rather than the end like traceback.

h <- function(x) {
  lobstr::cst()
}

f(x = 1)

#>     ▆
#>  1. └─global f(x = 1)
#>  2.   └─global g(x = 2)
#>  3.     └─global h(x = 3)
#>  4.       └─lobstr::cst()

Lazy Evaluation

A call stack often happens on a single branch when all arguments are eagerly evaluated. A more complex call stack can be created by including lazy evaluation.

a <- function(x) b(x)
b <- function(x) c(x)
c <- function(x) x

a(f())

#>     ▆
#>  1. ├─global a(f())
#>  2. │ └─global b(x)
#>  3. │   └─global c(x)
#>  4. └─global f()
#>  5.   └─global g(x = 2)
#>  6.     └─global h(x = 3)
#>  7.       └─lobstr::cst()

The tree gets 2 branches because x is lazily evaluated. First, a() calls b() and then b() calls c(). The second branch starts when c() evaluates its argument x. It gets a new branch as it is evaluated in the global environment and not the environment of c().

Frames

Each element of the call stack is a frame, an internal data structure with three key components:

  • An expression (labelled with expr) giving the function call. This is what traceback prints.
  • An environment (labelled with env) that is generally the execution environment.
  • A parent or the previous call in the call stack (shown with a grey arrow)

Environments as Data Structures

Environments are useful data structures as they have reference semantics. There are three common problems that this can help solve:

  • You will never accidentally create a copy of an environment which avoids copying large data.
  • Allows you to manage state across function calls within a package.
  • Simulates a hashmap or a data structure that takes a constant time to find an object based on its name.