-2

In Advanced R, environments are advertised as a useful way to get pass-by-reference semantics in R: instead of passing a list, which gets copied, I can pass an environment, which is not. This is useful to know.

But it assumes that whoever is calling my function is happy to agree on an "environment"-based data type, with named slots corresponding to the variables we want to modify.

Hasn't someone made a class which allows me to just refer to a single variable by reference? For example,

v = 1:5
r <- ref(v)
(function() {
    getRef(r)       # same as v
    setRef(r, 1:6)  # same as v <<- 1:6, in this case
})()

It would seem to be pretty easy to do this, by storing the character name of v together with the environment where it is bound.

Is there a standard library which accomplishes this semantics, or can someone provide a short snippet of code? (I haven't finished reading "Advanced R"; apologies if this is covered later in the book)

Metamorphic
  • 732
  • 6
  • 16
  • What is `setRef` and `getRef`? Please add all relevant code. The book may change in the future. – NelsonGon Jan 21 '20 at 07:04
  • 2
    Since you didn’t mention the data.table package or the R6 class, I’m thinking those may be useful areas of investigating. – IRTFM Jan 21 '20 at 07:09
  • Have a look at https://stackoverflow.com/q/2603184/10488504 – GKi Jan 21 '20 at 08:34

2 Answers2

2

As you have already mentioned in your question, you can store the variable name and its environment and access it with get and assign what will be somehow like a reference to a single variable.

v <- 1:5
r <- list(name="v", env=environment())
(function() {
    get(r$name, envir = r$env)
    assign(r$name, 1:6, envir = r$env)
})()
v
#[1] 1 2 3 4 5 6

Alternatively you can store the reference to an environment but then you can access everything in this referenced environment.

v <- 1:5
r <- globalenv() #reference to everything in globalenv
(function() {
    r$v
    r$v <- 1:6
})()
v
#[1] 1 2 3 4 5 6

You can also create an environment with only one variable and make a reference to it.

v <- new.env(parent=emptyenv())
v$v <- 1:5
r <- v
(function() {
    r$v
    r$v <- 1:6
})()
v$v
#[1] 1 2 3 4 5 6

Implemented as functions using find or set the environment during creation. Have also a look at How to get environment of a variable in R.

ref <- function(name, envir = NULL) {
  name <- substitute(name)
  if (!is.character(name)) name <- deparse(name)
  if(length(envir)==0) envir <- as.environment(find(name))
  list(name=name, envir=envir)
}
getRef <- function(r) {
  get(r$name, envir = r$envir, inherits = FALSE)
}
setRef <- function(r, x) {
  assign(r$name, x, envir = r$envir, inherits = FALSE)
}

x <- 1
r1 <- ref(x) #x from Global Environment

#x from Function Environment
r2 <- (function() {x <- 2; ref(x, environment())})()
#But simply returning x might here be better
r2b <- (function() {x <- 2; x})()

a <- new.env(parent=emptyenv())
a$x <- 3
r3 <- ref(x, a) #x from Environment a
GKi
  • 37,245
  • 2
  • 26
  • 48
  • Thank you, I didn't know about `as.environment(find(...))` – Metamorphic Jan 22 '20 at 21:05
  • Actually "find" is wrong to use here, it looks for a symbol in a package. I think I'll just answer my own question since it required some extra effort to get the code to work. – Metamorphic Jan 25 '20 at 01:57
0

This is based on GKi's answer, thanks to him for stepping up.

  • It includes pryr::where so you don't have to install the whole library
  • Note that we need to point "where" to parent.frame() in the definition of "ref"
  • Added some test cases which I used to check correctness

The code:

# copy/modified from pryr::where
where = function(name, env=parent.frame()) {
  if (identical(env, emptyenv())) {
    stop("Can't find ", name, call. = FALSE)
  }
  if (exists(name, env, inherits = FALSE)) {
    env
  } else {
    where(name, parent.env(env))
  }
}

ref <- function(v) {
  arg <- deparse(substitute(v))
  list(name=arg, env=where(arg, env=parent.frame()))
}

getRef <- function(r) {
  get(r$name, envir = r$env, inherits = FALSE)
}

setRef <- function(r, x) {
  assign(r$name, x, envir = r$env)
}

if(1) { # tests
  v <- 1:5
  r <- ref(v)
  (function() {
    stopifnot(identical(getRef(r),1:5))
    setRef(r, 1:6)
  })()
  stopifnot(identical(v,1:6))

  # this refers to v in the global environment
  v=2; r=(function() {ref(v)})()
  stopifnot(getRef(r)==2)
  setRef(r,5)
  stopifnot(getRef(r)==5)
  stopifnot(v==5)

  # same as above
  v=2; r=(function() {v <<- 3; ref(v)})()
  stopifnot(getRef(r)==3)
  setRef(r,5)
  stopifnot(getRef(r)==5)
  stopifnot(v==5)

  # this creates a local binding first, and refers to that. the
  # global binding is unaffected
  v=2; r=(function() {v=3; ref(v)})()
  stopifnot(getRef(r)==3)
  setRef(r,5)
  stopifnot(getRef(r)==5)
  stopifnot(v==2)

  # additional tests
  r=(function() {v=4; (function(v1) { ref(v1) })(v)})()
  stopifnot(r$name=="v1")
  stopifnot(getRef(r)==4)
  setRef(r,5)
  stopifnot(getRef(r)==5)

  # check that outer v is not modified
  v=2; r=(function() {(function(v1) { ref(v1) })(v)})()
  stopifnot(getRef(r)==2)
  setRef(r,5)
  stopifnot(getRef(r)==5)
  stopifnot(v==2)
}

I imagine there may be some garbage collection inefficiency if you're creating a reference to a small variable in a temporary environment with a different large variable, since the reference must retain the whole environment - although the same problem could arise with other uses of lexical scoping.

I will probably use this code next time I need pass-by-reference semantics.

Metamorphic
  • 732
  • 6
  • 16
  • When the variable is deleted from the stored environment the link is leading to the parrent. Try: `v <- 2; r <- (function() {v <- 3; ref(v)})(); getRef(r); rm(list=r$name, envir = r$env); getRef(r)` – GKi Jan 27 '20 at 16:26
  • How can you set a reference to a variable in an environment? E.g. `a <- new.env(parent=emptyenv()); a$x <- 3` how to set it to `x` in `a`? – GKi Jan 29 '20 at 07:26
  • Is that a feature request, like you want `ref` to have an optional argument for specifying the environment? But maybe it should also return a class with a print function and so on... I think what is here is enough for a SO answer. – Metamorphic Jan 29 '20 at 16:51