Introduction

Assuming that we are all familiar with classic R objects such as vectors, matrices, lists, data.frames, etc …, this chapter takes into consideration a critical type of objects: environments.

Within the R computation mechanism, environments play a crucial role as they are constantly used by R just behind the scene of interactive computation.

An environment is an object that takes care of mapping variable names to values. Each mapping is called a binding.

Being able to understand and manage environments represents a key step in the R programming learning curve.

Environments in R

The environment definition is clearly stated by in R Language Definition manual:

Environments can be thought of as consisting of two things.

  • A frame, consisting of a set of symbol-value pairs,
  • an enclosure, a pointer to an enclosing environment.

Given that a frame is a set of objects each of them associated to a name, where a name is a simple character string, in practice, we can consider an environment as a self contained portion of memory containing a frame. Each environment can access one and only one other environment known as the parent environment.

Environments in R are created, and eventually destroyed, under many circumstances.

Any R session has an environment associated known as the global environment. as returned by functions globalenv() and environment():

When we are working with R in interactive mode, we are using the frame within the globalenv as a container for our objects:

Any package has at least one environment:

Almost all functions have an environment as part of their definition:

User defined functions have an environment too:

Function environmentName() returns the name of an environment. As a result we may query R for the environment of function f():

or for the name of the environment associated to a package:

Unfortunately, function environmentName() does not always return the expected results:

Environment names for packages and namespaces are assigned at the C level. Therefore, user created environments do not reveal names. Users cannot set the name of an environment in R even through a, possibly misleadingly named, function called environmentName() exists. This function is really only meant for packages and namespaces, not other environments.

The ‘’environment tree structure’’

The definition of environment also states that an environment is made of an enclosure: a pointer to an enclosing environment. As a consequence, any environment has a parent environment that, as an environment has a parent environment. This chain of parent environments, known as the environment tree structure, roots to a special environment called the empty environment that, as stated by its name, contains no objects.

R has a very useful function, known as parent.env(), that returns the parent of any given environment:

In order to visualize the environment tree structure we can easily define a function that returns this structure starting from any given environment:

The above function make use of function Recall() that will be examined in the chapter dedicated to functions.

We can test tree() starting with globalenv() as argument:

Or we may want to use the built in functions search() that returns similar results

When we attach a list, usually a data.frame, we actually insert an entry in the environment tree structure in the position given by the pos argument of function attach(). As this parameter defaults to pos=2L, most of the times we attach just underneath the global environment:

When loading libraries, functions library() or require() work on a similar basis and use the same parameter pos = 2L

How R looks for objects

When R looks for any object, a symbol value pair, by default R looks for a matching symbol in the current environment and, if a matching symbol is found, the corresponding value is returned.

In case we want to search starting from a different environment we are usually able to specify it directly. As an example, we may consider the well known function get() that has an argument envir specifying which environment to search, at least as a starting point.

As a result, we can create an object named Formaldehyde in the current environment:

and use get() to find it along with the environment where to look for:

Note that an object with the same name exists in the environment of package:datasets and we can find it by specifying the right environment:

When R does not find the required symbol in the current environment, R looks in the parent environment and then in the parent of the parent until R either finds the symbol in any environment or reaches the empty environment. In the latest case, as by definition the empty environment contains no objects, R returns an error.

Given this search mechanism, R stops searching as soon as it finds an object with the corresponding name ignoring any object with the same name in any other environment in the environment tree structure.

This effect, known as masking, may result in quite embarrassing situations.

As a very simple example, suppose we define a simple function for computing circumference length given radius as argument:

and that, at any point of our working session we defined:

The result we would gain looks quite embarrassing:

In this case the object pi in the globalenv() :

masks the same symbol in the base environment

A robust method that reduce the risk of masking consists in specifying the package we are calling objects from: We could achieve this goal by using the ‘’::’’ operator:

Finally, any conflict is returned by:

Computing with Environments

As we have seen, environments are an essential components of the R working mechanism. As a consequence, it should not come as a surprise if environments are defined as R objects themselves.

As a consequence of being R objects, environments can be created:

and eventually deleted:

The frame component of an environment can be used as an objects place holder almost as we do with lists. We can place objects within an environment at least in three different ways:

by using the $ operator:

by using function with():

by using function assign:

Finally, we can browse environment env with standard functions ls() or ls.str() to check our result:

Suppose we want to store several objects at once into an environment, we may want to define a function fill_envir() that saves any series of objects within an environment:

The above function takes ... as argument and internally makes use of function Map() with an anonymous function as first argument. All this interesting concepts will be exhaustively explained in the next chapters.

By using function fill_env(), we can create a new environment and, subsequently, fill it with objects:

As we do with list(), we may also want a function envir() that directly creates an environment with named objects inside:

Note that, we have used the newly created function fill_envir() within the body of envir(); writing modular functions, reusable within new functions, is a key point for producing efficient R coding.

Function envir() is now ready to be used for creating new environments:

Up to now we have noticed that environments behave very similarly to lists but, at this point of this explanation, we must point out at least three differences that exists between environments and lists:

First of all, within environment all objects must have a name while lists do not impose this restriction. In fact, we can create a list with unnamed components:

but we cannot do the same with environments nor using function envir():

or, any other approach that attempts to create nameless objects within an environment.

This sound quite logical as the definition of environment states that: Environments consist of a frame, or collection of named objects.

Similarly, we may have lists with duplicated components:

This idea may look strange but it is a basic example masking within R.

When we try to repeat the same experiment with environments, we may observe a different behavior:

In this case, the second argument: x = 1 simply reassigns a different value to x.

Finally, as opposite to lists, within environments, the order objects were placed in does not matter. The frame is a collection of named objects and only names matter. As a consequence, objects of an environment are always displayed in alphabetical order:

The second part of the definition of environment states that an environment is made of an enclosure: a pointer to another environment.

As a consequence of this definition, when we create a new environment, it has, by definition, a parent environment.

Unless differently specified, the parent of the newly created environment is the environment where the environment was created.

As a result, if we create an environment, say env0, within the global environment, the latest results as the parent of env0.

We can pass env0 as an argument to the tree() function:

Note that, the name of the environment env0 is not returned by function environmentName(). This happens as the name of an environment is stored into the underlying C function and no assignment or replacement method exist, at the moment, for environments.

We may even create an environment, say env1 and specifically declare its parent environment:

Again, we can use function tree() to see the effects of the previous statement:

It should be clear at this point that the structure of the environment tree is a key element in the R programming mechanism.

These concepts will be very often recalled in the chapter dedicated to functions.

Copy on modify

When we do an assignment, R reserves a portion of memory for that object. We can display objects memory addresses by using a short function:

beside its cryptic output, mem_add() allows us to verify that, given two different objects:

they have different memory address:

and that, given:

if we assign

they share the same memory address:

that is: when vector x is copied into vector y, both objects share the same memory address.

When existing objects are modified, usually R objects follow a copy on modify semantic; that is the object is copied into a different memory address.

In practice, given an object:

with its address

if we modify it

R modify its address too

If we apply the same concept to lists, given a list

and its copy

both list share the same address

but, if we modify list1

list0 and list1 now have different addresses:

This mechanism: copy on modify, allows to preserve the value of list0 even if list1 is modified.

Prior to R 3.1 when modifying a list the entire list was copied. With version 3.1 we had a nice change that clearly helps in keeping memory usage under control.

Suppose we have two copied list made of more than one element:

and we modify only the second vector of the second list: y:

We can now observe that the memory address is modified only for the second element of the list while list0 and list1 keep sharing the same address for the first vector: x

In conclusion, we could say that R, at leat in its newest versions, uses a partial copy on modify semantic.

The same semantic does not apply to environments, that is environments do not copy on modify:

As an proof of concept, we can create an environment with some objects in it:

Afterward, we copy our newly created environment env0 into a second environment, say env1.

As env1 is a copy of env0, both environments contain the same symbols with the same values associated to them.

As environments do not copy on modify, if we now modify x within env1:

We can easily observe that the value of x within env0 is modified too:

The previous example clearly shows that any modification on env1 also affects env0. This is possible as env0 and env1 share the same memory address even after the modification env1$x <- 1:

Hashed environments

When we create a new environment, by setting hash=TRUE: the default value, we create a hashed environment.

In computer science, a hash table or hash map is a data structure that uses a hash function to map identifying values, known as keys, to associated values. Thus, a hash table implements an associative array. The hash function is used to transform the key into the index (the hash) of an array element (the slot or bucket) where the corresponding value is to be sought.

Hashed environment, allow value look up by symbol faster than traditional methods at the price of the hash table implementation.

As a proof of concept we may consider the following example.

First, we create a simple data frame whose rows represent name-value pairs:

Secondly, we create a new environment and we fill it with the name-value pairs so that we define, within the newly created environment, n objects of value i and name p.i:

As we can see, implementing the hash table require a certain amount of computing time.

We now define a random sample of names:

and finally, we want to create a vector out containing the values corresponding to each name. In practice, if we selected what <- c(p.1,p.2,p.3) we would like R to return c(1,2,3).

In order to achieve this result we may use either a crazy for loop approach

or the common R vectorized approach

or, finally, the new hash approach

Definitely, the hash approach, if we are willing to pay the computational price required for building the hash table, offers a clear advantage.