是否可以在 R 中将包环境附加为特定环境的父环境,而不是全局环境?

问题描述

我正在尝试创建一个用 R 编写的玩具 R REPL(here's代码)。理想情况下,我希望 REPL 本身在 R 终端中运行,但既不干扰也不依赖任何已经在全球环境中评估过的东西。不幸的是,我还没有能够想出解决这个问题的办法。我面临的主要挑战之一是如何附加包环境。

根据Hadley's Advanced Rlibrary()require() 附加的包成为全局环境的父项。然而,这意味着,如果我在我的玩具 REPL 中附加一个包,即使我没有在全局环境中运行它,它也会成为全局环境的父级。

例如(请注意,R> 提示符是“正常”的 R 终端,而 >>>> 是我的 REPL 的“终端”):

R> search()
#  [1] ".GlobalEnv"        "tools:rstudio"     "package:stats"    
#  [4] "package:graphics"  "package:Grdevices" "package:utils"    
#  [7] "package:datasets"  "package:methods"   "Autoloads"        
# [10] "package:base"     
R> replr::replr(env = new.env()) # new.env() defaults to having global environment as a parent
>>>> library(gtfsio)
>>>> rlang::env_parents(last = emptyenv())
#  [[1]] $ <env: global>
#  [[2]] $ <env: package:gtfsio>
#  [[3]] $ <env: tools:rstudio>
#  [[4]] $ <env: package:stats>
#  [[5]] $ <env: package:graphics>
#  [[6]] $ <env: package:Grdevices>
#  [[7]] $ <env: package:utils>
#  [[8]] $ <env: package:datasets>
#  [[9]] $ <env: package:methods>
# [[10]] $ <env: Autoloads>
# [[11]] $ <env: package:base>
# [[12]] $ <env: empty>
>>>> import_gtfs()
# Error: argument "path" is missing,with no default
>>>> q()
R> search()
#  [1] ".GlobalEnv"        "package:gtfsio"    "tools:rstudio"    
#  [4] "package:stats"     "package:graphics"  "package:Grdevices"
#  [7] "package:utils"     "package:datasets"  "package:methods"  
# [10] "Autoloads"         "package:base"  

我们可以看到我可以使用 gtfsio' import_gtfs() 函数(缺少 path,但你明白了),但是包也附加到了“main " R 终端。如果我尝试使用另一个环境作为我的新环境的父级,我什至无法访问包的函数,因为它无法找到它们,因为包环境成为全局环境的父级,而不是我的新环境:

Restarting R session...

R> search()
#  [1] ".GlobalEnv"        "tools:rstudio"     "package:stats"    
#  [4] "package:graphics"  "package:Grdevices" "package:utils"    
#  [7] "package:datasets"  "package:methods"   "Autoloads"        
# [10] "package:base" 
R> replr::replr(env = new.env(parent = baseenv()))
>>>> library(gtfsio)
>>>> rlang::env_parents()
# [[1]] $ <env: package:base>
# [[2]] $ <env: empty>
>>>> import_gtfs()
# Error: Could not find function "import_gtfs"
>>>> q()
R> search()
#  [1] ".GlobalEnv"        "package:gtfsio"    "tools:rstudio"    
#  [4] "package:stats"     "package:graphics"  "package:Grdevices"
#  [7] "package:utils"     "package:datasets"  "package:methods"  
# [10] "Autoloads"         "package:base"     

那么,有没有办法将包环境附加为自定义环境的父级,而不是全局环境?如果没有,有什么办法可以解决这个问题?

干杯!


编辑:

抱歉,我应该详细说明 REPL 的工作原理。

基本上,我只是使用 readline() 读取用户输入,将其解析为表达式并在指定环境中对其进行评估。下面的代码应该适用于一个简单的演示:

simple_repl <- function(env = new.env()) {
    
  while (TRUE) {
    
    input <- readline(">>>> ")
    
    if (input == "q()") break
    
    expr <- parse(text = input)
    
    result <- withVisible(
      eval(expr,envir = env)
    )
      
    if (result$visible)
      print(result$value)
      
  }
    
}

我在上面链接到我的 GitHub 的代码在处理某些情况时有点复杂,但这仍然是基本思想。

然后将 library() 调用评估为 eval(expression(library(gtfsio)),envir = env)

解决方法

您只有一个搜索路径,因此无法正确附加到另一个搜索路径。

您仍然可以拥有一系列父环境,我们可能会在您的 library 中重新定义 repl_env 以设置此链

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"  

如果您不想访问全局环境的对象,请使用 repl_env <- new.env(parent = parent.env(.GlobalEnv)) 作为第一行。

它永远不会是 100% 健壮的,但是,这是一个有趣的练习,但在认真对待它之前请仔细考虑。