Package-Wide Variables/Cache in R Packages

It’s often beneficial to have a variable shared between all the functions in an R package. One obvious example would be the maintenance of a package-wide cache for all of your functions. I’ve encountered this situation multiple times and always forget at least one important step in the process, so I thought I’d document it here for myself and anyone else who might encounter this issue. I setup a simple project on GitHub to demonstrate the various attempts you may take to solve the problem and, ultimately, a solution: https://github.com/trestletech/RCache. The rest of this article will presume some knowledge of authoring R packages. If that’s not you, check out RStudio’s guide to authoring R packages.

The fundamental problem comes down to R’s management of environments. (I’ll introduce the basics here, but for a thorough discussion on the topic, be sure to check out Hadley Wickham’s writings on the topic.) An environment is responsible for mapping data to named objects. When I execute

> a <- "hello"
> a
[1] "hello"

there was an environment responsible for initially associating the length-one character vector containing the text hello with the variable named a. Later, when I go to reference a variable by name, an environment is responsible for retrieving the data I had associated with this variable. Functions have their own environments in which they run, which is why you observe behavior like:

You can see that the variable named x, depending on which environment it’s in, can take one of two values in this example. Inside of the foo function, the x variable initially only existed in the parent environment, so it took on the value of x in that environment and was 1. Later, it was assigned a value of 2 and retained that value when it was returned. Outside of the function, however, x was associated with the value 1, and this binding was maintained despite another variable named x being created inside of a function’s environment.

This is a powerful feature of R, when properly understood. This allows you to create variables that only exist inside of a function or, more relevant to today’s discussion, only within a particular package. So it should be possible to leverage these environments to create a variable named cache which is accessible to all the functions in my package, but won’t accidentally likely be overwritten or modified by the user or, even more likely, collide with another variable named cache used in some other package.

For the rest of the demonstration, we’ll use the example of creating an R package which merely downloads files from the Internet. Of course, it might be appropriate to cache data in such a package so that the same URL won’t have to be retrieved remotely multiple times. One simple approach would be to use a list to associated character strings (such as URLs) with some data (such as the content of the web page). We can first create a function which will download data without using any cache:

Unfortunately, this, like our first example, is assigning data to a separate variable named cache which exists only inside of this function. If you were to build the package and run it, you’d find that the code:

would successfully download the book, Siddhartha, but the package cache would be empty. (If you’re unfamiliar with the ::: operator, it checks inside of the package — named on the left — for a variable — named on the right and returns it. So we can inspect the cache variable from code outside of our package.)

In order to alter a variable created in the parent environment, one must use the special assignment operator, <<-. This will adjust a binding not in the current environment, but in a parent environment. So we can adjust our line in which we assign a value to the cache variable to use this operator:

What’s the deal? R has a concept of “Locked Bindings” which allow you to forbiddiscourage changes in variables by locking either a particular variable binding or an entire environment. In this case, the cache binding has been locked and can’t be altered by constituent functions. So we’ll need to take a different approach altogether.

It seems we can properly access a package-wide variable from within a function of a package, but we’re not allowed to overwrite it (or create new variables at that level from within a function). Perhaps we could leverage environments to solve this problem. As it turns out, environments were likely a cleaner solution to our problem all along. Instead of creating a cache list, we can create it a cache environment:

cacheEnv <- new.env()

As long as this environment is created in one of our package’s .R files and not inside of a function, it will be accessible across our entire package. We can do all the things with this environment that we’re used to doing in our regular R environment (whether we knew we were using environments or not): create new variables (assign), modify existing variables (assign), remove variables (rm), retrieve data associated with a variable (get), list the variables in an environment (ls), etc.

All of the functions mentioned above accept an envir argument which specifies in which environment you’d like to perform the operation. The default is your current environment, but you could just as easily point these functions at your new environment to do something like assign(url, file, envir=cacheEnv) to assign the value currently stored in the file variable to a new variable who’s name is the value currently contained in the url variable within our cache environment. Then we could use get(url, envir=cacheEnv) to get the variable who name matches the current value of the url variable in the cache environment.

And we finally have a working cache. When a URL is requested via our package’s download function, the data will first be stored in this package’s cacheEnv environment before being returned. The next time that URL is requested, the cacheEnv environment will be checked to see if we already downloaded the content of that URL. Because a variable by that name already exists in our cacheEnv environment, the value will be pulled from there and returned rather than retrieved remotely.

Conclusion

Hopefully you’ve learned a thing or two about environments in R and how to use them from within R packages. Environments can be very valuable tools for advanced R programmers. They’re one of the tried-and-true ways to replicate “pass-by-reference” programming in R (though that’s typically unexpected — thus discouraged — behavior for an R object). They also have some unique lookup properties being built on hashes that allow them to more expediently map character strings to data when there is a very large number of possible keys.