StephGC
StephGC

Reputation: 69

How to capture a function's environment instead of its return value

Given a function creating several objects in its temporary environment:

myFun <- function(arg1 = "hey", arg2 = "bye") {
     x <- paste(arg1, arg2)
     y <- paste(x, "hey")
     z <- paste(y, "again!")
     
     return(z)
}

Is there a function that can extract the temporary environment of another function ? e.g.

myFun_env <- capture_env(myFun())

That would return as output the equivalent to:

myFun_env <- rlang::env(
     arg1 = "hey",
     arg2 = "bye",
     x = paste(arg1, arg2),
     y = paste(x, "hey"),
     z = paste(y, "again!")
 )
> rlang::env_print(myFun_env)
<environment: 0x556959d73708>
parent: <environment: global>
bindings:
 * x: <chr>
 * y: <chr>
 * z: <chr>
 * arg1: <chr>
 * arg2: <chr>

Update 1: Solution from user G. Grothendieck https://stackoverflow.com/users/516548/g-grothendieck

> trace(myFun, quote(assign(".Env", environment(), .GlobalEnv)), print = FALSE)
> myFun()
> rlang::env_print(.Env)
<environment: 0x556bf1a8ec08>
parent: <environment: global>
bindings:
 * z: <chr>
 * y: <chr>
 * x: <chr>
 * arg1: <chr>
 * arg2: <chr>
> .Env$arg1
[1] "hey"

This modifies the function's body, but can be removed via untrace:

> body(myFun)
{
    .doTrace(assign(".Env", environment(), .GlobalEnv))
    {
        x <- paste(arg1, arg2)
        y <- paste(x, "hey")
        z <- paste(y, "again!")
        return(z)
    }
}
> untrace(myFun)
> body(myFun)
{
    x <- paste(arg1, arg2)
    y <- paste(x, "hey")
    z <- paste(y, "again!")
    return(z)
}

>

Update 2: Wrapping things up, we could have a function that captures another function's environment, returns it and writes it to disk for further loading:

captureAndSave_functionEnv <- function (myFun, ...) {
  
  trace(myFun, quote(assign(".myFun_env", environment(), .GlobalEnv)), print = FALSE)
  #trace(myFun, quote({ untrace(myFun); assign(".myFun_env", environment(), parent.frame()) }), print = FALSE)
  on.exit(untrace(myFun))
  
  myFun(...)
  
  saveRDS(.myFun_env, paste0(deparse(substitute(myFun)), ".rds"))
  
  return(.myFun_env)
}
> captureAndSave_functionEnv(myFun)
<environment: 0x558f877eff20>
> myFun_env_loaded <- readRDS("myFun.rds")
> rlang::env_print(myFun_env_loaded)
<environment: 0x558f86d00890>
parent: <environment: global>
bindings:
 * z: <chr>
 * y: <chr>
 * x: <chr>
 * arg1: <chr>
 * arg2: <chr>
> myFun_env_loaded$arg1
[1] "hey"

Upvotes: 1

Views: 340

Answers (3)

G. Grothendieck
G. Grothendieck

Reputation: 269556

Use trace to inject a statement that captures the environment as a side effect:

trace(myFun, quote(assign(".Env", environment(), .GlobalEnv)), print = FALSE)

myFun()
## [1] "hey bye hey again!"

ls(.Env)
## [1] "arg1" "arg2" "x"    "y"    "z"   

Upvotes: 2

Konrad Rudolph
Konrad Rudolph

Reputation: 545588

After the function is called, the function’s call environment (its “stack frame”) no longer exists, unless the lifetime of that frame was somehow extended, by being referenced from somewhere.1

So, no, there’s no way to get the environment after the fact from outside. What you can do is modify the function:

myFun <- function(arg1 = "hey", arg2 = "bye") {
     x <- paste(arg1, arg2)
     y <- paste(x, "hey")
     z <- paste(y, "again!")

     environment()
}

Here we return the call frame instead of z.


1 If this isn’t done explicitly, the environment is generally not referenced anywhere. However, there are exceptions: both functions and formulas by default reference the environment they were defined in. As a consequence, if you define a function or formula inside myFun() and return that, you can retrieve the call frame of a myFun(…) invocation by calling environment() on the return value.

Upvotes: 2

Hong Ooi
Hong Ooi

Reputation: 57686

Yes, call the environment function inside your function.

f <- function()
{
    x <- 42
    environment()
}
r$> f()
<environment: 0x0000020a1302a5a8>
r$> f()$x
[1] 42

Upvotes: 1

Related Questions