Source Script to Separate Environment in R, Not the Global Environment

Source script to separate environment in R, not the global environment

The following environment insertion appears to achieve the desired functionality:

Check the current search path:

search()
# [1] ".GlobalEnv" "package:stats" "package:graphics"
# [4] "package:grDevices" "package:utils" "package:datasets"
# [7] "package:methods" "Autoloads" "package:base"

Add new environment for sourced packages and use local parameter when source()ing:

myEnv <- new.env()    
source("some_other_script.R", local=myEnv)
attach(myEnv, name="sourced_scripts")

Check the search path:

search()
# [1] ".GlobalEnv" "sourced_scripts" "package:dplyr"
# [4] "package:stats" "package:graphics" "package:grDevices"
# [7] "package:utils" "package:datasets" "package:methods"
# [10] "Autoloads" "package:base"

Note that we attach() the new environment after sourcing, so that dplyr is attached after our script environment in the search path.

Separate scripts from .GlobalEnv: Source script that source scripts

You can use trace to inject code in functions,
so you could force all source calls to set local = TRUE.
Here I just override it if local is FALSE in case any nested calls to source actually set it to other environments due to special logic of their own.

env <- new.env()

# use !isTRUE if you want to support older R versions (<3.5.0)
tracer <- quote(
if (isFALSE(local)) {
local <- TRUE
}
)

trace(source, tracer, print = FALSE, where = .GlobalEnv)

# if you're doing this inside a function, uncomment the next line
#on.exit(untrace(source, where = .GlobalEnv))

source("main.R", local = env)

As mentioned in the code,
if you wrap this logic in a function,
consider using on.exit to make sure you untrace even if there are errors.

EDIT: as mentioned in the comments,
this could have issues if some of the scripts you will be loading assume there is 1 (global) environment where everything ends.
I suppose you could change the tracer to something like

tracer <- quote(
if (missing(local)) {
local <- TRUE
}
)

or maybe

tracer <- quote(
if (isFALSE(local)) {
# fetch the specific environment you created
local <- get("env", .GlobalEnv)
}
)

The former assumes that if the script didn't specify local at all,
it doesn't care about which environment ends up holding everything.
The latter assumes that source calls that didn't specify local or set it to FALSE want everything to end up in 1 environment,
and modify the logic to use your environment instead of the global one.

Is it possible to move a variable from the global environment into a separate environment?

Maybe:

library(purrr)

a <- 111
b <- 'hello'

my_envir <- new.env()

names(.GlobalEnv) %>%
walk(~ assign(.x, get(.x), envir = my_envir))

eapply(my_envir, function(x) x)
#> $my_envir
#> <environment: 0x7fed59e56dc8>
#>
#> $a
#> [1] 111
#>
#> $b
#> [1] "hello"

Or

library(purrr)
a <- 111
b <- 'hello'
my_envir <- new.env()

eapply(.GlobalEnv, function(x) x) %>%
discard(is.environment) %>%
{walk2(., names(.), ~{
assign(.y, .x, envir = my_envir)
exec('rm', .y, envir = .GlobalEnv)}
)}

eapply(my_envir, function(x) x)
#> $a
#> [1] 111
#>
#> $b
#> [1] "hello"

Created on 2021-12-31 by the reprex package (v2.0.1)

How to overwrite .GlobalEnv with other environment

1) To copy all elements of an environment to the global environment convert that environment to a list using as.list and then copy the contents of that list to the global environment using list2env. Whether you really need to do this or not is not clear. Could you not just leave the objects in that hidden environment?

list2env(as.list(myEnv), .GlobalEnv)
X
## [1] 42

2) I don't really recommend this but rather than modifying the global environment it is possible to place myEnv on the search path and then you can refer to its objects as long as they are not masked by objects of the same name in the global environment.

X <- 43
attach(myEnv)

X
## 43

get("X", "myEnv")
## 42

rm(X)
X
## 42

Note

Note that to create a hidden environment in your package you can just put this into the source code of the package and not export it.

myEnv <- new.env()

For example, the lattice package creates a hidden environment called .latticeEnv in this source file: https://github.com/cran/lattice/blob/master/R/zzz.R

Cleaning up the global environment after sourcing: How to remove objects of a certain type in R

As an alternative approach (similar to @Ken's suggestion from the comments), the following code allows you to delete all objects created after a certain point, except one (or more) that you specify:

freeze <- ls() # all objects created after here will be deleted
var <- letters
for (i in var) {
assign(i, runif(1))
}
df <- data.frame(x1 = a, x2 = b, x3 = c)
rm(list = setdiff(ls(), c(freeze, "df"))) #delete old objects except df

The workhorse here is setdiff(), which will return a list a list of the items that appear in the first list but not the second. In this case, all items created after freeze except df. As an added bonus, freeze is deleted here as well.

Is it possible to attach package environments as parents of a specific environment, not the global environment, in R?

You have only one search path so there's no way to properly attach to another one.

You can still have a chain of parent environments though, we might redefine library in your repl_env to set up this chain

repl_env <- new.env()
with(repl_env, library <- function(package) {
# fetch repl_env from the inside
repl_env <- parent.env(environment())
# and its parent (.GlobalEnv the first time)
parent_env <- parent.env(repl_env)
# create a new env for our package and fill it
pkg_env <- new.env()
package <- deparse1(substitute(package))
object_nms <- getNamespaceExports(package)
objects <- mget(object_nms, envir = asNamespace(package))
list2env(objects, pkg_env)
# stitch it above repl_env and below repl_env's parent
parent.env(pkg_env) <- parent_env
parent.env(repl_env) <- pkg_env
# base::library returns the search path invisibly but here it woudn't make
# sense so we just return NULL
invisible(NULL)
})

simple_repl(repl_env)
>>>> x <- "hello"
>>>> y <- "world"
>>>> library(glue)
>>>> glue("{x} {y}")
#> hello world
>>>>

# the {glue} package is not on the search path
search()
#> [1] ".GlobalEnv" "tools:rstudio" "package:stats" "package:graphics"
#> [5] "package:grDevices" "package:utils" "package:datasets" "package:methods"
#> [9] "Autoloads" "package:base"

Use repl_env <- new.env(parent = parent.env(.GlobalEnv)) as your first line if you don't want to have access to the global environment's objects.

It will never be 100% robust however, it was a fun exercise but think carefully before doing something serious with this.



Related Topics



Leave a reply



Submit