15

I have written a function that sources files that contain scripts for other functions and stores these functions in an alternative environment so that they aren't cluttering up the global environment. The code works, but contains three instances of eval(parse(...)):

# sourceFunctionHidden ---------------------------
# source a function and hide the function from the global environment
sourceFunctionHidden <- function(functions, environment = "env", ...) {
    if (environment %in% search()) {
        while (environment %in% search()) {
            if (!exists("counter", inherits = F)) counter <- 0
            eval(parse(text = paste0("detach(", environment, ")")))
            counter <- counter + 1 
        }
        cat("detached", counter, environment, "s\n")
    } else {cat("no", environment, "attached\n")}
    if (!environment %in% ls(.GlobalEnv, all.names = T)) {
        assign(environment, new.env(), pos = .GlobalEnv)
        cat("created", environment, "\n")
    } else {cat(environment, "already exists\n")}
    sapply(functions, function(func) {
        source(paste0("C:/Users/JT/R/Functions/", func, ".R"))
        eval(parse(text = paste0(environment, "$", func," <- ", func)))
        cat(func, "created in", environment, "\n")
    })
    eval(parse(text = paste0("attach(", environment, ")")))
    cat("attached", environment, "\n\n")
}

Much has been written about the sub-optimality of the eval(parse(...)) construction (see here and here). However, the discussions that I've found mostly deal with alternate strategies for subsetting. The first and third instances of eval(parse(...)) in my code don't involve subsetting (the second instance might be related to subsetting).

Is there a way to call new.env(...), [environment name]$[function name] <- [function name], and attach(...) without resorting to eval(parse(...))? Thanks.

N.B.: I don't want to change the names of my functions to .name to hide them in the global environment

Josh
  • 1,210
  • 12
  • 30
  • 1
    Just discovered that `eval(parse(text = paste0("detach(", environment, ")")))` can be replaced with `detach(environment, character.only = T)`. The question about improving `eval(parse(text = paste0("attach(", environment, ")")))` remains. – Josh Mar 30 '19 at 02:03

3 Answers3

5

For what its worth, the function source actually uses eval(parse(...)), albeit in a somewhat subtle way. First, .Internal(parse(...)) is used to create expressions, which after more processing are later passed to eval. So eval(parse(...)) seems to be good enough for the R core team in this instance.

That said, you don't need to jump through hoops to source functions into a new environment. source provides an argument local that can be used for precisely this.

local: TRUE, FALSE or an environment, determining where the parsed expressions are evaluated.

An example:

env = new.env()
source('test.r', local = env)

testing it works:

env$test('hello', 'world')
# [1] "hello world"
ls(pattern = 'test')
# character(0)

And an example test.r file to use this on:

test = function(a,b) paste(a,b)
dww
  • 30,425
  • 5
  • 68
  • 111
  • Thank you, I missed that aspect of `source()`. However, if I change that line of code to `source(paste0("C:/Users/JT/R/Functions/", func, ".R"), local = environment)` I get the error `Error in source(paste0("C:/Users/JT/R/Functions/", func, ".R"), local = environment) : 'local' must be TRUE, FALSE or an environment`. Is there a way to convert the `"env"` that comes from `environment` to `env`? – Josh Mar 30 '19 at 02:11
  • You should create an environment to save into. For example as I demonstrated with `env = new.env()`. Then pass the environment as your argument. If you need to name the new environement using a character string (`environemt` in your example - although it is bad practice to use reserved words as names), you can use `assign(environment, new.env())` – dww Mar 30 '19 at 02:34
3

If you want to keep it off global_env, put it into a package. It's common for people in the R community to put a bunch of frequently used helper functions into their own personal package.

thc
  • 9,527
  • 1
  • 24
  • 39
  • 1
    It's not as hard you think! I think the function you're trying to write is a lot harder and more complicated. Lots of tutorials to write packages out there. – thc Mar 30 '19 at 03:49
  • I haven't had time to make a package yet, but if [this description](https://hilaryparker.com/2014/04/29/writing-an-r-package-from-scratch/) of how easy it is is accurate, holy crap! I'm going to make everything into a package! – Josh Mar 30 '19 at 04:04
0

tl;dr: The right way to convert quoted strings to object names is to use assign() and get(). See this post.

The long answer: The answer from @dww about being able to source() directly to a specific environment led me to change the second instance of eval(parse(...)) as follows:

# old version
source(paste0("C:/Users/JT/R/Functions/", func, ".R"))
eval(parse(text = paste0(environment, "$", func," <- ", func)))
# new version
source(
    paste0("C:/Users/JT/R/Functions/", func, ".R"), 
    local = get(environment)
)

The answer from @dww also got me to exploring attach(). attach() has an argument that allows specification of the environment to which to direct the output. This led me to change the third instance of eval(parse(...)) (below). Note the use of get() to convert the "env" that comes from environment to the unquoted env that attach() requires.

# old version
eval(parse(text = paste0("attach(", environment, ")")))
# new version
attach(get(environment), name = environment)

Finally, at some point in this process I was reminded that rm() has a character.only argument. detach() accepts the same argument, so I changed the second instance of eval(parse()) as below:

# old version
eval(parse(text = paste0("detach(", environment, ")")))
# new version
detach(environment, character.only = T)

So my new code is:

# sourceFunctionHidden ---------------------------
# source a function and hide the function from the global environment
sourceFunctionHidden <- function(functions, environment = "env", ...) {
    if (environment %in% search()) {
        while (environment %in% search()) {
            if (!exists("counter", inherits = F)) counter <- 0
            detach(environment, character.only = T)
            counter <- counter + 1 
        }
        cat("detached", counter, environment, "s\n")
    } else {cat("no", environment, "attached\n")}
    if (!environment %in% ls(.GlobalEnv, all.names = T)) {
        assign(environment, new.env(), pos = .GlobalEnv)
        cat("created", environment, "\n")
    } else {cat(environment, "already exists\n")}
    sapply(functions, function(func) {
        source(
            paste0("C:/Users/JT/R/Functions/", func, ".R"), 
            local = get(environment)
        )
        cat(func, "created in", environment, "\n")
    })
    attach(get(environment), name = environment)
    cat("attached", environment, "\n\n")
}
Josh
  • 1,210
  • 12
  • 30