2

I am working on a function to basically grab arguments from inside a function. It works as expected, but when I try and transport the result, I have an envir in the list. It outputs fine via dput, but I CANNOT transport it.

> test(iris)
list(envir = <environment>, call = test(iris), fn.info = list(
    fn = "test", dot.keys = NULL, dot.vals = NULL, params = list(
        df = "iris"), map = list(df = "iris"), formals = list(
        df = "--EMPTY--")))

As you can see, the INTERNALS grab the fn function name, and function parameters. One element captured is the envir which is simple to reproduce as follows:

dput( (envir = environment()) );

The list at least TRAPPED the environment, but when I try and paste it back into a session, I get an error:

> x = list(envir = <environment>, call = test(iris), fn.info = list(
Error: unexpected '<' in "x = list(envir = <"
>     fn = "test", dot.keys = NULL, dot.vals = NULL, params = list(
Error: unexpected ',' in "    fn = "test","
>         df = "iris"), map = list(df = "iris"), formals = list(
Error: unexpected ')' in "        df = "iris")"
>         df = "--EMPTY--")))
Error: unexpected ')' in "        df = "--EMPTY--")"


Ideally, I would like to pair of functions that would cast it to/from envir as a string, so I can transport it.

Question: How to transport an environment via dput?

env.toString(envir) {}

env.fromString(envir) {}

The above represents my current roadmap, but I am open to suggestions. I have tried as.character , as.raw, as.hexmode... I believe deparse internally will work, but that will not enable me to recover the environment as an object without the propose env.fromString

Dharman
  • 30,962
  • 25
  • 85
  • 135
mshaffer
  • 959
  • 1
  • 9
  • 19
  • perhaps `dput(eapply(someenv, c))` (or `saveRDS(eapply(someenv, c), "path/to/file.rds")`). (This would need to be done for each env within the list, should be feasible starting with `sapply(mylist, is.environment)` to replace the contents in your list.) – r2evans Oct 01 '22 at 00:41
  • 1
    The documentation for `dput()` makes clear that it's not the right tool for what's being proposed and to use `save()` or `saveRDS()`. That said, you might get away with serializing the environments in your list and unserializing to restore them, so an approach (assuming a flat list) could look like: `out_dat <- dput(lapply(my_list, \(x) if (is.environment(x)) serialize(x, NULL) else x)); in_dat <- lapply(out_dat, \(x) if(is.raw(x)) unserialize(x) else x)`. – Ritchie Sacramento Oct 01 '22 at 02:52

3 Answers3

3

As you've noted, dput() doesn't provide the code necessary to rebuild an environment

env1 <- new.env()
env1$x <- "something"
env2 <- asNamespace("utils")
env3 <- .GlobalEnv
test <- list(env1, env2, env3, 1, "a") 
dput(test)
#> list(<environment>, <environment>, <environment>, 1, "a")

An essential property of environments is they have a parent, so to reproduce an environment you might have to reproduce its parent, and the parent of its parent etc. Thus it is not possible (or reasonable ?) to tackle the general case.

Some environments however already have a name in R, or are easy to call, using asNamespace(), it would be reasonable to expect dput() to do it but it doesn't.

The {constructive} package does though and can provide a "sometimes good enough" solution in the more general case by using as.environment() on lists.

constructive::construct(test, check = FALSE)
#> list(as.environment(list(x = "something")), asNamespace("utils"), .GlobalEnv, 1, "a")

The argument check = FALSE is to prevent it to fail since the object that would be recreated through the code is not strictly identical to the input.

It will soon be on CRAN but meanwhile : https://github.com/cynkra/constructive


pragmatically you might try :

env.toString <- function (envir) {
  sprintf("as.environment(%s)", paste(deparse(as.list(envir)), collapse = "\n"))
}
moodymudskipper
  • 46,417
  • 11
  • 121
  • 167
  • Indeed, `R` is a half-baked cake. You can't cast `closures` or `envir` to/from STRINGS. – mshaffer Oct 08 '22 at 03:13
  • Your `env.toString` function does some interesting things. Try it with `envir = environment ( mean )` – mshaffer Oct 08 '22 at 03:15
  • It has many limitations, it's just a shot in the dark to help OP if they don't need all this env talk and have simple list like envs to dput. {constructive} has much more ambition. – moodymudskipper Oct 18 '22 at 07:39
1

Perhaps you could use ls() to get the names of all objects in the environment, and then put their names and values in a list, then reconstruct an environment from that list with list2env()... Something like:

x <- new.env()
x$foo <- 1:3

xnames <- ls(envir = x)
xlist <- lapply(xnames, function(y) {
  z <- x[[y]]
  if (!inherits(z, 'environment'))
    return(z)
})
names(xlist) <- xnames

dput(list(my_env = xlist))
#> list(my_env = list(foo = 1:3))

a_list <- list(my_env = list(foo = 1:3))

x2 <- list2env(a_list$my_env)
print(x2$foo)
#> [1] 1 2 3
Andrew Brown
  • 1,045
  • 6
  • 13
1

If you just need to remove the items from a list that are an environment you can just iterate over the list remove the elements that inherit from 'environment'

x <- list(a = new.env(), b = "foo")
dput(x[-sapply(x, inherits, 'environment')])
#> list(b = "foo")
Andrew Brown
  • 1,045
  • 6
  • 13