1

I would like to write a function that returns an environment containing a function which assigns the value of an object inside the environment. For example, what I want to do is:

makeenv <- function() {
  e <- new.env(parent = .GlobalEnv)
  e$x <- 0
  e$setx <- function(k) { e$x <- k }  # NOT OK
  e
}

I would like to fix the e$setx function above. The behavior of the above is weird to me:

e1 <- makeenv()
e1$x
## [1] 0
e1$setx
## function(k) e$x <- k
## <environment: 0x7f96144d8240>
e1$setx(3)     # Strangely, this works.
e1$x
## [1] 3
# --------- clone ------------
e2 <- new.env(parent = .GlobalEnv)
e2$x <- e1$x
e2$setx <- e1$setx
e2$x
## [1] 3
# ----- e2$setx() changes e1$x -----
e2$setx(7)   # HERE
e2$x         # e2$x is not changed.
## [1] 3
e1$x         # e1$x is changed instead.
## [1] 7

Could someone please help me understand what is going on here? I especially don't understand why e2$setx(7) sets e1$x to 7 rather than issuing an error. I think I am doing something very wrong here.

I would also like to write a correct function e$setx inside the makeenv function that correctly assigns a value to the x object in the environment e. Would it be possible to have one without using S4 or R6 classes? I know that a function like setx <- function(e,k) { e$x <- k } works, but to me e1$setx(5) looks more intuitive than setx(e1,5) and I would like to investigate this possibility first. Is it possible to have something like e$setx <- function(k) { self$x <- k }, say, where self refers to the e preceding the $?

This page The equivalent of 'this' or 'self' in R looks relevant, but I like to have the effect without using S4 or R6. Or am I trying to do something impossible? Thank you.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
chan1142
  • 609
  • 4
  • 13

1 Answers1

3

You can use local to evaluate the function definition in the environment:

local(etx <- function (k) e$x <- k, envir = e)

Alternatively, you can also change the function’s environment after the fact:

e$setx <- function(k) e$x <- k
environment(e$setx) <- e

… but neither is strictly necessary in your case, since you probably don’t need to create a brand new environment. Instead, you can reuse the current calling environment. Doing this is a very common pattern:

makeenv <- function() {
  e <- environment()
  x <- 0
  setx <- function(k) e$x <- k
  e
}

Instead of e$x <- k you could also write e <<- k; that way, you don’t need the e variable at all:

makeenv <- function() {
  x <- 0
  setx <- function(k) x <<- k
  environment()
}

… however, I actually recommend against this: <<- is error-prone because it looks for assignment targets in all parent environments; and if it can’t find any, it creates a new variable in the global environment. It’s better to explicitly specify the assignment target.


However, note that none of the above changes the observed semantics of your code: when you copy the function into a new environment, it retains its old environment! If you want to “move over” the function, you explicitly need to reassign its environment:

e2$setx <- e1$setx
environment(e2$setx) <- e2

… of course writing that entire code manually is pretty error-prone. If you want to create value semantics (“deep copy” semantics) for an environment in R, you should wrap this functionality into a function:

copyenv <- function (e) {
  new_env <- list2env(as.list(e, all.names = TRUE), parent = parent.env(e), hash = TRUE)
  new_env$e <- new_env
  environment(new_env$setx) <- new_env
  new_env
}
e2 <- copyenv(e1)

Note that the copyenv function is not trying to be general; it needs to be adapted for other environment structures. There is no good, general way of writing a deep-copy function for environments that handles all cases, since a general function can’t know how to handle self-references (i.e. the e in the above): in your case, you want to preserve self-references (i.e. change them to point to the new environment). But in other cases, the reference might need to point to something else.

This is a general problem of deep copying. That’s why the R serialize function, for instance, has refHook parameter that tells the function how to serialise environment references.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • Thank a lot! The `copyenv` function works for the `makeenv` using `<<-`. It is also possible to make it work for the `makeenv` beginning with `e <- environment()`? – chan1142 Apr 07 '21 at 12:49
  • 1
    @chan1142 Oh right, I had forgotten that. Yes, it’s possible but it’s unfortunately a bit messy. See update. – Konrad Rudolph Apr 07 '21 at 13:10
  • Thanks! I see `new_env$e <- new_env`. Fascinating! Also thanks for the detailed explanation. Very helpful. – chan1142 Apr 07 '21 at 13:27