Apostolos Polymeros
Apostolos Polymeros

Reputation: 843

Copy/move one environment to another

I would like to ask if it is possible to copy/move all the objects of one environment to another, at once. For example:

f1 <- function() {
    print(v1)
    print(v2)
}

f2 <- function() {
    v1 <- 1
    v2 <- 2

    # environment(f1)$v1 <- v1         # It works
    # environment(f1)$v2 <- v2         # It works
    
    environment(f1) <- environment(f2) # It does not work
}

f2()
f1()

Upvotes: 26

Views: 12286

Answers (7)

moodymudskipper
moodymudskipper

Reputation: 47340

The other current solutions who actually attempt to make a copy will all fail if the environment contains promises, because they convert environments to lists.

{rlang} has now env_clone() which will clone the environment, including promises and active bindings. It doesn't do a deep copy though, and clones the environment by default with a new parent, you can do rlang::env_clone(env, parent.env(env)) to keep the same parent though.

I recommend the above, find below a previous solution from when {rlang} had more limitations. It should behave similar but provides an option for deep copy, with caveats given at the bottom.


The solution below works in these cases. Following @geoffrey-poole 's idea I propose an argument to deep copy or not, and I showcase the function on a test case.

It uses unexported functions from the package {pryr}. I don't know of a base R equivalent.

The function

clone_env <- function(env, deep = FALSE) {
  is_unevaled_promise <- function(name, env) {
    pryr:::is_promise2(name, env) && !pryr:::promise_evaled(name, env)
  }
  # create new environment with same parent
  clone <- new.env(parent = parent.env(env))
  for(obj in ls(env, all.names = TRUE)) {
    promise_lgl <- is_unevaled_promise(as.symbol(obj), env = env)
    if(promise_lgl) {
      # fetch promise expression and env
      promise_expr <- pryr:::promise_code(obj, env)
      promise_env  <- pryr:::promise_env(obj, env)

      # Assign this expression as a promise (delayed assignment) in our
      # cloned environment
      eval(bquote(
        delayedAssign(obj, .(promise_expr), eval.env = promise_env, assign.env = clone)))
    } else {
      obj_val <- get(obj, envir = env)
      if(is.environment(obj_val) && deep) {
        assign(obj, clone_env(obj_val, deep = TRUE),envir= clone)
      } else  {
        assign(obj, obj_val, envir= clone)
      }
    }
  }
  attributes(clone) <- attributes(env)
  clone
}

Shallow copy

Let's build an environment containing a character variable, a promise (note that a is undefined), and a nested environment.

create_test_env <- function(x = a){
  y <- "original"
  nested_env <- new.env()
  nested_env$nested_value <- "original"
  environment()
}
env <- create_test_env()
ls(env)
#> [1] "nested_env" "x"          "y"

# clone it, with deep = FALSE
shallow_clone <- clone_env(env, deep = FALSE) 
#> Registered S3 method overwritten by 'pryr':
#>   method      from
#>   print.bytes Rcpp
ls(shallow_clone)
#> [1] "nested_env" "x"          "y"

# the promise was copied smoothly
a <- 42
shallow_clone$x
#> [1] 42

# We can change values independently
shallow_clone$y <- "modified"
env$y
#> [1] "original"

# except if we have nested environents!
shallow_clone$nested_env$nested_value <- "modified"
env$nested_env$nested_value
#> [1] "modified"

Deep copy

Let's do it all over again, but with a deep clone now, we see the nested values are distinct this time.

env <- create_test_env()
deep_clone <- clone_env(env, deep = TRUE) 
a <- 42
deep_clone$x
#> [1] 42
deep_clone$y <- "modified"
env$y
#> [1] "original"
deep_clone$nested_env$nested_value <- "modified"
env$nested_env$nested_value
#> [1] "original"

Created on 2020-09-10 by the reprex package (v0.3.0)


Note that the deep copy is not super robust:

  • We should recursively explore every list object and look for environments to clone there too.
  • What if we find .GlobalEnv, .BaseNamespaceEnv etc there, do we really want to clone those ? We could have an argument to feed arguments not to clone
  • Attributes might contain environments too, and these are not deep copied

I haven't met the need to go further in the rabbit hole. I you need this feel free to edit or copy and improve into your own answer.

Upvotes: 10

Geoffrey Poole
Geoffrey Poole

Reputation: 1298

The "clone" method posted by Tommy won't make a true (deep) clone when e1 contains names that reference other environments. For instance, if e1$nestedEnv references an environment, e2$nestedEnv will reference the same environment, not a copy of that environment. Thus, the name e1$nestedEnv$bar will reference the same memory location as e2$nestedEnv$bar and any new value assigned to e1$nestedEnv$bar will be reflected for e2$nestedEnv$bar as well. This may be desirable behavior, but calling e2 a clone of e1 could be misleading.

Here is a function that will allow the user to make either a copy of an environment while also copying any nested environments (a "deep clone", using deep = TRUE), or just use the method proposed by Tommy to copy the environment while maintaining original references to any nested environments (using deep = FALSE).

The 'deep = TRUE' method uses rapply to recursively call cloneEnv on nested environment within envir, for as many levels as the environments are nested. So, in the end, it recursively calls rapply, which is a bit of a mind bender, but works pretty well.

Note that if a nested environment contains a name that references a parent environment, using the "deep" method will never return from the recursive calls. If I could have figured out a way to check for this, I would have included it...

Note, too, that environments can have attributes, so copying the attributes would be necessary for a true clone, which this solution also addresses.

cloneEnv <- function(envir, deep = T) {
  if(deep) {
    clone <- list2env(rapply(as.list(envir, all.names = TRUE), cloneEnv, classes = "environment", how = "replace"), parent = parent.env(envir))
  } else {
    clone <- list2env(as.list(envir, all.names = TRUE), parent = parent.env(envir))
  }
  attributes(clone) <- attributes(envir)
  return(clone)
}

An example:

Create environment e1, which also contains a nested environment:

e1 <- new.env()
e1$foo <- "Christmas"
e1$nestedEnv <- new.env()
e1$nestedEnv$bar <- "New Years"

Show the values for foo and bar:

e1$foo
[1] "Christmas"
e1$nestedEnv$bar
[1] "New Years"

Make a deep clone (i.e. e2 contains makes a copy of nestedEnv)

e2 <- cloneEnv(e1, deep = TRUE)

nestedEnv in e1 references a difference environment than nestedEnv in e2:

identical(e1$nestedEnv, e2$nestedEnv)
[1] FALSE

But the values are the same because e2$nestedEnv is a copy of e1$nestedEnv:

e2$foo
[1] "Christmas"
e2$nestedEnv$bar
[1] "New Years"

Change the values in e2:

e2$foo <- "Halloween"
e2$nestedEnv$bar <- "Thanksgiving"

And values in e1 remain unchanged, again, because e1$nestedEnv points to a different environment than e2$nestedEnv:

e1$foo
[1] "Christmas"
e2$foo
[1] "Halloween"

e1$nestedEnv$bar
[1] "New Years"
e2$nestedEnv$bar
[1] "Thanksgiving"

Now, re-create e1 using Tommy's method:

e2 <- cloneEnv(e1, deep = FALSE)

nestedEnv in e2 points to the same environment as nestedEnv in e1:

identical(e1$nestedEnv, e2$nestedEnv)
[1] TRUE

Update the values in e2 and e2's nestedEnv:

e2$foo <- "Halloween"
e2$nestedEnv$bar <- "Thanksgiving"

Values of foo are independent:

e1$foo
[1] "Christmas"
e2$foo
[1] "Halloween"

but updating value e2's bar has also updated e1's bar because e1$nestedEnv and e2$nestedEnv reference (point to) the same environment.

e1$nestedEnv$bar
[1] "Thanksgiving"
e2$nestedEnv$bar
[1] "Thanksgiving"

Upvotes: 6

Neal Fultz
Neal Fultz

Reputation: 9687

I use this function in my package to copy objects:

copyEnv <- function(from, to, names=ls(from, all.names=TRUE)) {
  mapply(assign, names, mget(names, from), list(to), 
         SIMPLIFY = FALSE, USE.NAMES = FALSE)
  invisible(NULL)
}

Upvotes: 3

Brunno Oliveira
Brunno Oliveira

Reputation: 19

To do it:

environment(f1) <- environment(f2) # It does not work

Open the f1 environment and run do this:

ls(load(f2))

Upvotes: 0

Tommy
Tommy

Reputation: 40861

There seem to be at least 3 different things you can do:

  1. Clone an environment (create an exact duplicate)
  2. Copy the content of one environment to another environment
  3. Share the same environment

To clone:

# Make the source env
e1 <- new.env()
e1$foo <- 1
e1$.bar <- 2   # a hidden name
ls(e1) # only shows "foo"

# This will clone e1
e2 <- as.environment(as.list(e1, all.names=TRUE))

# Check it...
identical(e1, e2) # FALSE
e2$foo
e2$.bar

To copy the content, you can do what @gsk showed. But again, the all.names flag is useful:

# e1 is source env, e2 is dest env
for(n in ls(e1, all.names=TRUE)) assign(n, get(n, e1), e2)

To share the environment is what @koshke did. This is probably often much more useful. The result is the same as if creating a local function:

f2 <- function() {
  v1 <- 1 
  v2 <- 2

  # This local function has access to v1 and v2
  flocal <- function() {
    print(v1)
    print(v2)
  }

  return(flocal)
} 

f1 <- f2()
f1() # prints 1 and 2 

Upvotes: 33

Ari B. Friedman
Ari B. Friedman

Reputation: 72769

You could use assign:

f1 <- function() {
  print(v1)
  print(v2)
}

f2 <- function() {
  v1 <- 1
  v2 <- 2

  for(obj in c("v1","v2")) {
    assign(obj,get(obj),envir=f1.env)
  }
}

If you don't want to list out the objects, ls() takes an environment argument.

And you'll have to figure out how to get f1.env to be an environment pointing inside f1 :-)

Upvotes: 5

kohske
kohske

Reputation: 66902

Try this:

f2 <- function() {
    v1 <- 1
    v2 <- 2
    environment(f1) <<- environment()
}

Upvotes: 9

Related Questions