The environment is the data structure that powers scoping. This chapter dives deep into environments, describing their structure in depth, and using them to improve your understanding of the four scoping rules described in lexical scoping. Understanding environments is not necessary for day-to-day use of R. But they are important to understand because they power many important R features like lexical scoping, namespaces, and R6 classes, and interact with evaluation to give you powerful tools for making domain specific languages, like dplyr and ggplot2.
If you can answer the following questions correctly, you already know the most important topics in this chapter. You can find the answers at the end of the chapter in answers.
List at least three ways that an environment is different to a list.
What is the parent of the global environment? What is the only environment that doesn’t have a parent?
What is the enclosing environment of a function? Why is it important?
How do you determine the environment from which a function was called?
Environment basics introduces you to the basic properties of an environment and shows you how to create your own.
Recursing over environments provides a function template for computing with environments, illustrating the idea with a useful function.
Explicit environments briefly discusses three places where environments are useful data structures for solving other problems.
This chapter will use rlang functions for working with environments, because it allows us to focus on the essence of environments, rather than the incidental details.
Note that the
env_ functions in rlang are designed to work with the pipe: all take an environment as the first argument, and many also return an environment. I won’t use the pipe in this chapter in the interest of keeping the code as simple as possible, but you should consider it for your own code.
7.2 Environment basics
Generally, 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 (i.e., it doesn’t make sense to ask what the first element of an environment is).
An environment has a parent.
Environments are not copied when modified.
Let’s explore these ideas with code and pictures.
To create an environment, use
rlang::env(). It works like
list(), taking a set of name-value pairs:
new.env() to creates a new environment. Ignore the
size parameters; they are not needed. Note that you can not simultaneously create and define values; use
$<-, as shown below.
The job of an environment is to associate, or bind, a set of names to a set of values. You can think of an environment as a bag of names, with no implied order (i.e. it doesn’t make sense to ask which is the first element in an environment). For that reason, we’ll draw the environment as so:
As discussed in names and values, environments have reference semantics: unlike most R objects, when you modify them, you them modify in place, and don’t create a copy. One important implication is that environments can contain themselves. This means that environments go one step further in their level of recursion than lists: an enviroment can contain any object, including itself!
Printing an evironment just displays its memory address, which is not terribly useful:
Instead, we’ll use
env_print() which gives us a little more information:
You can use
env_names() to get a character vector giving the current bindings
In R 3.2.0 and greater, use
names() to list the bindings in an environment. If your code needs to work with R 3.1.0 or earlier, use
ls(), but note that the default value of
FALSE so you don’t see any bindings that start with
7.2.2 Important environments
We’ll talk in detail about special environments in Special environments, but for now we need to mention two. The current environment, or
current_env() is the environment in which code is currently executing. When you’re experimenting interactively, that’s usually the global environment, or
global_env(). The global environment is sometimes called your “workspace”, as it’s where all interactive (i.e. outside of a funtion) computation takes place.
Note that to compare environments, you need to use
identical() and not
Access the global environment with
globalenv() and the current environment with
environment(). The global environment is printed as
Every environment has a parent, another environment. In diagrams, the parent is shown as a small pale blue circle and arrow that points to another environment. The parent is what’s used to implement lexical scoping: if a name is not found in an environment, then R will look in its parent (and so on).
You can set the parent environment by supplying an unnamed argument to
env(). If you don’t supply it, it defaults to the current environment.
We use the metaphor of a family to name environments relative to one another. The grandparent of an environment is the parent’s parent, and the ancestors include all parent environments up to the empty environment. To save space, I typically won’t draw all the ancestors; just remember whenever you see a pale blue circle, there’s an parent environment somewhere.
You can find the parent of an environment with
Only one environment doesn’t have a parent: the empty environment. I draw the empty environment with a hollow parent environment, and where space allows I’ll label it with
R_EmptyEnv, the name R uses.
You’ll get an error if you try and find the parent of the empty environment:
You can list all ancestors of an environment with
env_parents() continues until it hits either the global environment or the empty environment. You can control this behaviour with the
parent.env() to find the parent of an environment. No base function returns all ancestors.
7.2.4 Getting and setting
You can get and set elements of a environment with
[[ in the same way as a list:
But you can’t use
[[ with numeric indices, and you can’t use
[[ will return
NULL if the binding doesn not exist. Use
env_get() if you want an error:
If you want to use a default value if the binding doesn’t exist, you can use the
There are two other ways to add bindings to an environment:
env_poke()6 takes a name (as string) and a value:
env_bind()allows you to bind multiple values:
You can determine if an environment has a binding with
Unlike lists, setting an element to
NULL does not remove it. Instead, use
Unbinding a name doesn’t delete the object. That’s the job of the garbage collector, which automatically removes objects with no names binding to them. This process is described in more detail in GC.
rm(). These are designed interactively for use with the current environment, so working with other environments is a little clunky. Also beware the
inherits argument: it defaults to
TRUE meaning that the base equivalents will inspect the supplied environment and all its ancestors.
Add something once rlang has an API. Also mention in data structures below
7.2.6 Advanced bindings
There are two more exotic variants of
env_bind_exprs()creates delayed bindings, which are evaluated the first time they are accessed. Behind the scenes, delayed bindings create promises, so behave in the same way as function arguments.
Delayed bindings are used to implement
autoload(), which makes R behave as if the package data is in memory, even though it’s only loaded from disk when you ask for it.
env_bind_fns()creates active bindings which are re-computed every time they’re accessed:
The argument to the function allows you to also override behaviour when the variable is set:
List three ways in which an environment differs from a list.
Create an environment as illustrated by this picture.
Create a pair of environments as illustrated by this picture.
e[c("a", "b")]don’t make sense when
eis an environment.
Create a version of
env_poke()that will only bind new names, never re-bind old names. Some programming languages only do this, and are known as single assignment languages.
7.3 Recursing over environments
If you want to operate on every ancestor of an environment, it’s often convenient to write a recursive function. This section shows you how, applying your new knowledge of environments to write a function that given a name, finds the environment
where() that name is defined, using R’s regular scoping rules.
The definition of
where() is straightforward. It has two arguments: the name to look for (as a string), and the environment in which to start the search. (We’ll learn why
caller_env() is a good default in calling environments.)
There are three cases:
The base case: we’ve reached the empty environment and haven’t found the binding. We can’t go any further, so we throw an error.
The successful case: the name exists in this environment, so we return the environment.
The recursive case: the name was not found in this environment, so try the parent.
These three cases are illustrated with these three examples:
It might help to see a picture. Imagine you have two environments, as in the following code and diagram:
where(a, e4a)will find
where("b", e4a)doesn’t find
e4a, so it looks in its parent,
e4b, and finds it there.
where("c", e4a)looks in
e4b, then hits the empty environment and throws an error.
It’s natural to work with environments recursively, so
where() provides a useful template. Removing the specifics of
where() shows the structure more clearly:
where()to return all environments that contain a binding for
name. Carefully think through what type of object the function will need to return.
Write a function called
fget()that finds only function objects. It should have two arguments,
env, and should obey the regular scoping rules for functions: if there’s an object with a matching name that’s not a function, look in the parent. For an added challenge, also add an
inheritsargument which controls whether the function recurses up the parents or only looks in one environment.
7.4 Special environments
Most environments are not created by you (e.g. with
env()) but are instead created by R. In this section, you’ll learn about the most important environments, starting with the package environments. You’ll then learn then about the function environment bound to the function when it is created, and the (usually) ephemeral execution environment created every time the function is called. Finally, you’ll see how the package and function environments interact to support namespaces, which ensure that a package always behaves the same way, regardless of what other packages the user has loaded.
7.4.1 Package environments and the search path
Each package attached by
require() becomes one of the parents of the global environment. The immediate parent of the global environment is the last package you attached7:
And the parent of that package is the second to last package you attached:
If you follow all the parents back, you see the order in which every package has been attached. This is known as the search path because all objects in these environments can be found from the top-level interactive workspace.
search_envs() #> [] $ <env: global> #> [] $ <env: package:rlang> #> [] $ <env: package:methods> #> [] $ <env: package:stats> #> [] $ <env: package:graphics> #> [] $ <env: package:grDevices> #> [] $ <env: package:utils> #> [] $ <env: package:datasets> #> [] $ <env: Autoloads> #> [] $ <env: base>
You can access the names of the environments on the search path with
The last two environments on the search path are always the same:
Autoloadsenvironment uses delayed bindings to save memory by only loading package objects (like big datasets) when needed.
The base environment,
package:baseor sometimes just
base, is the environment of the base package. It is special because it has to be able to bootstrap the loading of all other packages. You can access it directly with
Graphically, the search path looks like this:
When you attach another package with
library(), the parent environment of the global environment changes:
7.4.2 The function environment
A function binds the current environment when it is created. This is called the function environment, and is used for lexical scoping. Across computer languages, functions that capture their environments are called closures, which why this term is often used interchangeably with function in R’s documentation.
You can get the function environment with
environment(f) to access the environment of function
In diagrams, I’ll depict functions as rectangles with a rounded end that binds an environment.
In this case,
f() binds the environment that binds the name
f to the function. But that’s not always the case: in the following example
g is bound in a new environment
g() binds the global environment. The distinction being binding and being bound by is subtle but important; the difference is how we find
g vs. how
g finds its variables.
In the diagram above, you saw that the parent environment of a package varies based on what other packages have been loaded. This seems worrying: doesn’t that mean that the package will find different functions if packages are loaded in a different order? The goal of namespaces is to make sure that this does not happen, and that every package works the same way regardless of what packages are attached by the user.
For example, take
sd() is defined in terms of
var(), so you might worry that the result of
sd() would be affected by any function called
var() either in the global environment, or in one of the other attached packages. R avoids this problem by taking advantage of the function vs. binding environment described above. Every function in a package is associated with a pair of environments: the package environment, which you learned about earlier, and the namespace environment.
The package environment is the external interface to the package. It’s how you, the R user, find a function in an attached package or with
::. Its parent is determined by search path, i.e. the order in which packages have been attached.
The namespace environment is the internal interface to the package. The package environment controls how we find the function; the namespace controls how the function finds its variables.
Every binding in the package environment is also found in the namespace environment; this ensures every function can use every other function in the package. But some bindings only occur in the namespace environment. These are known as internal or non-exported objects, which make it possible to hide internal implementation details from the user.
Every namespace environment has the same set of ancestors:
Each namespace has an imports environment that contains bindings to all the functions used by the package. The imports environment is controlled by the package developer with the
Explicitly importing every base function would be tiresome, so the parent of the imports environment is the base namespace. The base namespace contains the same bindings as the base environment, but it has different parent.
The parent of the base namespace is the global environment. This means that if a binding isn’t defined in the imports environment the packge will look for it in the usual way. This is usually a bad idea (because it makes code depend on other loaded packages), so
R CMD checkautomatically warns about such code. It is needed primarily for historical reasons, particularly due to how S3 method dispatch works.
Putting all these diagrams together we get:
sd() looks for the value of
var it always finds it in a sequence of environments determined by the package developer, but not by the package user. This ensures that package code always works the same way regardless of what packages have been attached by the user.
Note that there’s no direct link between the package and namespace environments; the link is defined by the function environments.
7.4.4 Execution environments
The last important topic we need to cover is the execution environment. What will the following function return the first time it’s run? What about the second?
Think about it for a moment before you read on.
This function returns the same value every time because of the fresh start principle, described in a fresh start. Each time a function is called, a new environment is created to host execution. This is called the execution environment, and its parent is the function environment. Let’s illustrate that process with a simpler function. I’ll draw execution environments with an indirect parent; the parent environment is found via the function environment.
The execution environment is usually ephemeral; once the function has completed, the environment will be GC’d. There are several ways to make it stay around for longer. The first is to explicitly return it:
Another way to capture it is to return an object with a binding to that environment, like a function. The following example illustrates that idea with a function factory,
plus(). We use that factory to create a function called
There’s a lot going on in the diagram because the enclosing environment of
plus_one() is the execution environment of
What happens when we call
plus_one()? Its execution environment will have the captured execution environment of
plus() as its parent:
You’ll learn more about function factories in functional programming.
Draw a diagram that shows the enclosing environments of this function:
Write an enhanced version of
str()that provides more information about functions. Show where the function was found and what environment it was defined in.
7.5 The call stack
There is one last environment we need to explain, the caller environment. To explain this environment we need to introduce two new concepts: the call stack and frames. Executing a function creates two types of context. You’ve learn about one already: the execution environment is a child of the function environment, which is determined by where the function was created. There’s another type of context created by where the function was called: this is called the call stack.
There are also a couple of small wrinkles when it comes to custom evaluation. See environments vs. frames for more details.
7.5.1 Simple call stacks
Let’s illustrate this with a simple sequence of calls:
The way you most commonly see a call stack in R is after by looking at the
traceback() after an error has occured:
traceback() to understand the call stack, we’re going to use
lobstr::cst() to print out the call stack tree:
This shows us that
cst() was called from
h(), which was called from
g(), which was called from
f(). Note that the order is the opposite from
traceback(). As the call stacks get more compliated, I think it’s easier to understand the sequence of calls if you start from the beginning, rather than the end (
g(), rather than
g() was called by
7.5.2 Lazy evaluation
The call stack above is simple - while you get a hint that there’s some tree-like structure involved, everything happens on a single branch of the tree. This is typical of a call stack when all arguments are eagerly evaluated.
Let’s create a more complicated example that involves some lazy evaluation. We’ll create a sequence of functions,
c(), that call each passing along an argument
x is lazily evaluated so this tree gets two branches. In the first branch
c(). The second branch starts when
c() evaluates its argument
x. This argument is evaluated in a new call stack because it needs to behave in the same way as if it had been evaluated in the top level environment.
Each element of the call stack is a frame8, also known as an evaluation context. The frame is an extremely important internal data structure, and R code can only access a small part of the data structure because it’s so critical. A frame has three main components that are accessible from R:
An expression (labelled with
expr) giving the function call. This is what
An environment (labelled with
env), which is typically the execution environment of a funtion. There are two main exceptions: the environment of the global frame is the global environment, and calling
eval()also generates frames, where the environment can be anything.
A parent, the previous call in the call stack (shown by a grey arrow).
(To focus on the calling environments, I have omitted the bindings in the global environment from
h to the respective function objects.)
The frame also holds exit handlers created with
on.exit(), restarts and handlers for the the condition system, and which context to
return() to when a function completes. These are important for the internal operation of R, but are not directly accessible.
7.5.4 The caller environment
While it’s possible to access any frame environment in the call stack, there is one only environment that you routinely should: caller environment, which you can access with
caller_env(). This is a good default for an user directed function that needs an environment argument.
parent.frame() is equivalent to
caller_env(); just note that it returns an environment, not a frame.
Looking up variables in the calling stack rather than in the enclosing environment is called dynamic scoping. Few languages implement dynamic scoping (Emacs Lisp is a notable exception.) This is because dynamic scoping makes it much harder to reason about how a function operates: not only do you need to know how it was defined, you also need to know the context in which it was called. Dynamic scoping is primarily useful for developing functions that aid interactive data analysis. It is one of the topics discussed in non-standard evaluation.
7.6 As data structures
As well as powering scoping, environments are also useful data structures in their own right because they have reference semantics. There are three common problems that they can help solve:
Avoiding copies of large data. Since environments have reference semantics, you’ll never accidentally create a copy. This makes it a useful vessel for large objects. Bare environments are not that pleasant to work with; I recommend using R6 objects instead. Learn more in R6.
Managing state within a package. Explicit environments are useful in packages because they allow you to maintain state across function calls. Normally, objects in a package are locked, so you can’t modify them directly. Instead, you can do something like this:
Returning the old value from setter functions is a good pattern because it makes it easier to reset the previous value in conjunction with
on.exit()(see more in on exit).
As a hashmap. A hashmap is a data structure that takes constant, O(1), time to find an object based on its name. Environments provide this behaviour by default, so can be used to simulate a hashmap. See the CRAN package hash for a complete development of this idea.
The ancestors of an environment have an important relationship to
<<-. The regular assignment arrow,
<-, always creates a variable in the current environment. The deep assignment arrow,
<<-, never creates a variable in the current environment, but instead modifies an existing variable found by walking up the parent environments.
<<- doesn’t find an existing variable, it will create one in the global environment. This is usually undesirable, because global variables introduce non-obvious dependencies between functions.
<<- is most often used in conjunction with a closure, as described in Closures.
What does this function do? How does it differ from
<<-and why might you prefer it?
7.8 Quiz answers
There are four ways: every object in an environment must have a name; order doesn’t matter; environments have parents; environments have reference semantics.
The parent of the global environment is the last package that you loaded. The only environment that doesn’t have a parent is the empty environment.
The enclosing environment of a function is the environment where it was created. It determines where a function looks for variables.
<-always creates a binding in the current environment;
<<-rebinds an existing name in a parent of the current environment.
You might wonder why rlang has
env_set(). This is for consistency:
_set()functions return a modified copy;
_poke()functions modify in place.↩
Note the difference between attached and loaded. A package is loaded automatically if you access one of its functions using
::; it is only attached to the search path by
?environmentuses frame in a different sense: “Environments consist of a frame, or collection of named objects, and a pointer to an enclosing environment.”. We avoid this sense of frame, which comes from S, because it’s very specific and not widely used in base R. For example, the “frame” in
parent.frame()is an execution context, not a collection of named objects.↩