I’ve been working on a codebase that relied on storing a lot of objects in R environments, mainly because of the potential speed improvements with large numbers of objects.
See this article for a pretty good explanation.
After a recent spec change, I needed to start looping around a block of code that was previously using a single environment to store objects. The easiest approach was to create an initial base environment to use at the start of each iteration of the loop, and then create a copy of that environment that would be specific to the loop iteration.
But I got a surprise!
All of the result environments looked like the last iteration.
First, let’s look at how this works when you start with a base list, make a copy, and modify the copy:
> original <- list() > copy <- original
Because copy
hasn’t been modified, it shares the same memory
address as original
, for efficiency:
tracemem(original) [1] "<0x4c5d738>" > tracemem(copy) [1] "<0x4c5d738>"
So let’s modify copy
, and notice that it’s now copied to its own memory address:
> copy$item <- 1L tracemem[0x4c5d738 -> 0x5a4c1b8]:
And if we now compare original
and copy
, we get completely expected differences:
> original list() > copy $item [1] 1 > original$item NULL > identical(original, copy) [1] FALSE
Now let’s repeat with environments:
> original <- new.env() > copy <- original
And if we use tracemem
as before, we get a hint that something is different:
> tracemem(original) Error in tracemem(original) : 'tracemem' is not useful for promise and environment objects
So if we simply print the environments, we’ll see the memory addresses:
> original <environment: 0x69d6cc8> > copy <environment: 0x69d6cc8> > identical(original, copy) [1] TRUE
Now let’s modify copy
, and notice that the memory address doesn’t change:
> copy$item <- 1L > original <environment: 0x69d6cc8> > copy <environment: 0x69d6cc8>
Presumably that means that original
also now has an element named item
, right?
> copy$item [1] 1 > original$item [1] 1 > identical(original, copy) [1] TRUE
Right!
So if we remove the element item
from original
, it should also disappear from copy
:
> rm('item', pos = as.environment(original)) > original$item NULL > names(original) character(0) > copy$item NULL > identical(original, copy) [1] TRUE
So, if you’ve ever copied an existing environment thinking you could modify it independently of the original, you should revisit that code to make sure it’s not causing problems!