GoGonzo
GoGonzo

Reputation: 2867

Create environment having access to libraries but not to .GlobalEnv

I would like to evaluate some code in the environment which will have the access to libraries (all environments above .GlobalEnv) but won't have access to the objects created in the .GlobalEnv. I've tried couple solutions but none seemed to work as expected

1. new environment in .GlobalEnv

Here environment created in the .GlobalEnv have an access to the other objects in the .GlobalEnv.

.GlobalEnv$ee <- environment()
eval( 
  parse(text = "library(dplyr);mutate(iris, x = 1)"), 
  envir = .GlobalEnv$ee
)
var_in_global <- "x"
eval(
  expr = parse(text = "ls()"), 
  envir = .GlobalEnv$ee
) # expect empty
eval(
  expr = parse(text = "print(var_in_global)"), 
  envir = ee
) # expect error

2. New environment as a child of .GlobalEnv

This one is Coded different way but result is th same as 1st

ee <- new.env(parent = globalenv())
eval(
  parse(text = "library(dplyr);mutate(iris, x = 1)"), 
  envir = ee
)
var_in_global <- "x"
eval(
  expr = parse(text = "ls()"), 
  envir = ee
)
eval(
  expr = parse(text = "print(var_in_global)"), 
  envir = ee
)

3. New environment as parent of global

In this case environment doesn't have an access to the .GlobalEnv objects but libraries loaded after will be attached below, so the environment doesn't have an access to these libraries as well.

ee <- new.env(parent = parent.env(globalenv()))
eval(
  parse(text = "library(dplyr);mutate(iris, x = 1)"), 
  envir = ee
)
var_in_global <- "x"
eval(
  expr = parse(text = "ls()"), 
  envir = ee
)
eval(
  expr = parse(text = "print(var_in_global)"), 
  envir = ee
)

4. Using library(..., pos = 3)

Using answer from @Allan Cameron, I've tried to make this code working. And library(dplyr);mutate(...) were correctly evaluated in new environment.

ee <- new.env(parent = parent.env(globalenv()))
eval(
  parse(text = "library <- function(...) base::library(..., pos = 3)"),
  envir = ee
)
eval(
  parse(text = "library(dplyr);mutate(iris, x = 1)"), 
  envir = ee
)
var_in_global <- "x"
eval(
  expr = parse(text = "ls()"), 
  envir = ee
)
eval(
  expr = parse(text = "print(var_in_global)"), 
  envir = ee
)

However, problem is much deeper. Some packages have more dependencies which are loaded along. Consider this example where I've replaced library(dplyr);mutate() with library(Hmisc);impute(...). This example fails - couldn't find impute function (which is wrong)

ee <- new.env(parent = parent.env(globalenv()))
eval(
  parse(text = "library <- function(...) base::library(..., pos = 3)"),
  envir = ee
)
eval(
  parse(text = "library(Hmisc);impute(iris[,1], 1)"), 
  envir = ee
) # expect to work

Do you have any ideas how to make an environment which will be "parallel" node to the global, and still have libraries attached before?

Upvotes: 3

Views: 411

Answers (1)

Allan Cameron
Allan Cameron

Reputation: 173803

The problem is that when you call library, by default it attaches the package at pos = 2 in the search path:

pos
the position on the search list at which to attach the loaded namespace. Can also be the name of a position on the current search list as given by search().

So, when I start an R session and do search(), I get:

#>  [1] ".GlobalEnv"        "tools:rstudio"     "package:stats"    
#>  [4] "package:graphics"  "package:grDevices" "package:utils"    
#>  [7] "package:datasets"  "package:methods"   "Autoloads"        
#> [10] "package:base"

And when I call library(dplyr) then repeat search(), I get:

#>  [1] ".GlobalEnv"        "package:dplyr"     "tools:rstudio"    
#>  [4] "package:stats"     "package:graphics"  "package:grDevices"
#>  [7] "package:utils"     "package:datasets"  "package:methods"  
#> [10] "Autoloads"         "package:base"  

So, if you make ee have the same parent as the Global environment before attaching any packages, you will have the problem that the packages are placed between the Global Environment and ee's entry into the search path.

There are a few ways round this, but perhaps the simplest is to start a new R session and define:

library <- function(...) base::library(..., pos = 3)

Which ensures that packages loaded in the Global Workspace are always placed after ee's "entry point" to the search path. This produces the desired behaviour:

ee <- new.env(parent = parent.env(globalenv()))

var_in_global <- "x"

eval(expr = parse(text = "ls()"), envir = ee)
#> character(0)

eval(expr = parse(text = "print(var_in_global)"), envir = ee)
#> Error in print(var_in_global): object 'var_in_global' not found

library(dplyr) # Note this is called in the global environment

eval(parse(text = "head(mutate(iris, n = 1), 5)"), envir = ee)
#>   Sepal.Length Sepal.Width Petal.Length Petal.Width Species n
#> 1          5.1         3.5          1.4         0.2  setosa 1
#> 2          4.9         3.0          1.4         0.2  setosa 1
#> 3          4.7         3.2          1.3         0.2  setosa 1
#> 4          4.6         3.1          1.5         0.2  setosa 1
#> 5          5.0         3.6          1.4         0.2  setosa 1

Note that as a (possibly desirable) side effect, since the modified library function is defined in the global environment, then if library is called from inside ee, it will be base::library that is dispatched, and any packages loaded within ee will therefore only be accessible to ee.

EDIT

If you want code called within ee to affect the global search path as well as ee's search path, you could try:

ee <- new.env(parent = parent.env(globalenv()))

ee$library <- function(...) {
  mc <- match.call()
  mc[[1]] <- quote(base::library)
  eval(mc, envir = globalenv())
  this_env <- parent.frame()
  if(!identical(this_env, globalenv())) 
  parent.env(this_env) <- parent.env(globalenv())
}

This gives us:

eval(
  parse(text = "library(Hmisc);impute(iris[,1], 1)"), 
  envir = ee
)  

#>  [1] 5.1 4.9 4.7 4.6 5.0 5.4 4.6 5.0 4.4 4.9 5.4 4.8 4.8 4.3 5.8 5.7 5.4 5.1 5.7
#>  [20] 5.1 5.4 5.1 4.6 5.1 4.8 5.0 5.0 5.2 5.2 4.7 4.8 5.4 5.2 5.5 4.9 5.0 5.5 4.9
#>  [39] 4.4 5.1 5.0 4.5 4.4 5.0 5.1 4.8 5.1 4.6 5.3 5.0 7.0 6.4 6.9 5.5 6.5 5.7 6.3
#>  [58] 4.9 6.6 5.2 5.0 5.9 6.0 6.1 5.6 6.7 5.6 5.8 6.2 5.6 5.9 6.1 6.3 6.1 6.4 6.6
#>  [77] 6.8 6.7 6.0 5.7 5.5 5.5 5.8 6.0 5.4 6.0 6.7 6.3 5.6 5.5 5.5 6.1 5.8 5.0 5.6
#>  [96] 5.7 5.7 6.2 5.1 5.7 6.3 5.8 7.1 6.3 6.5 7.6 4.9 7.3 6.7 7.2 6.5 6.4 6.8 5.7
#> [115] 5.8 6.4 6.5 7.7 7.7 6.0 6.9 5.6 7.7 6.3 6.7 7.2 6.2 6.1 6.4 7.2 7.4 7.9 6.4
#> [134] 6.3 6.1 7.7 6.3 6.4 6.0 6.9 6.7 6.9 5.8 6.8 6.7 6.7 6.3 6.5 6.2 5.9

However, we need to be careful. If library is called in the global environment, this will not update the search path for ee. So we would need to have a global function like:

library <- function(...) {
  base::library(...)
  parent.env(ee) <- parent.env(globalenv())
}

Of course, it would be much nicer to have your own package do this. That way, you can have a single library function which tests its calling frame and dispatches the appropriate method, without having these spare library functions floating around the workspaces.

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

Upvotes: 4

Related Questions