rstateenvironment

Modifying state for `par()` and environmental variables using withr


I have a legacy function that relies on objects not passed in as input arguments. I cannot change that function.

To document them, I'd like to temporarily change the state in an isolated environment. I'd like to use the withr package for that.

However, I can't quite seem to get this to work the way I want to.

Calling ls(), the variables I'd like to assign temporarily (example_text and example_df) remain in the execution environment.

What do I need to do so they are removed too? I need to also use par() here. Is there a way to do this already or do I need to write a new withr function that combines the cleanup of setting graphical parameters and temporarily defined variables?

# Bad example function in a library that relies on parameters defined in
# global environemnt
bad_function <- function(){
  plot(example_df)
}
par_1 <- par()
# Execute
withr::with_par(
  new = list(oma = c(rep(2, 4)), cex = 0.55),
  code = {
    example_df <- mtcars
    example_text <- "whatever"
    bad_function()
    text(x = 1, y = 2,labels = example_text)
  }
)

picture of function output that plots example_df

par_2 <- par()
# parameters are cleaned up
identical(par_1, par_2)
#> [1] TRUE
# variables `example_df` and `example_text` that I do not want to show up still do
ls()
#> [1] "bad_function" "example_df"   "example_text" "par_1"        "par_2"

Created on 2025-07-08 with reprex v2.1.1


Solution

  • 1) Wrap the withr(...) in a local and set the environment of a copy of the bad_function to that of the environment of the local -- just 3 extra lines added to the code in the question and marked by ## plus we have change the text line to mtext to improve the example.

    bad_function <- function(){
      plot(example_df)
    }
    
    local({ ##
      withr::with_par(
        new = list(oma = c(rep(2, 4)), cex = 0.55),
        code = {
          example_df <- mtcars
          example_text <- "whatever"
          environment(bad_function) <- environment() ##
          bad_function()
          mtext(example_text, line = 5) ##
        }
      )
    }) ##
    
    ls()
    ## [1] "bad_function"
    

    2) proxy environment The code above works for the reproducible example in the question but if in the actual situation the body of bad_function references additional objects in environment(bad_function) as suggested in a comment then use this. It creates a proxy environment e which contains example_df whose parent is environment(bad_function) and resets environment(bad_function) to that.

    good2 <- function(example_df, example_text) {
      e <- new.env(parent = environment(bad_function))
      e$example_df <- example_df
      environment(bad_function) <- e
      withr::with_par(
        new = list(oma = c(rep(2, 4)), cex = 0.55),
        code =  { bad_function(); mtext(example_text, line = 5) }
      )
    }
    
    good2(mtcars, "whatever")
    ls()
    

    3) proto A variation of (2) which is slightly more concise can be developed using the proto package. This creates a proto object (an environment with certain methods) whose parent is environment(bad_function). Inserting a function into it as shown will change the environment of that function to that of the proto object eliminating one step.

    library(proto)
    
    good3 <- function(example_df, example_text) {
      p <- proto(environment(bad_function), example_df = example_df,
        bad_function = bad_function)
      withr::with_par(
        new = list(oma = c(rep(2, 4)), cex = 0.55),
        code = { with(p, bad_function)(); mtext(example_text, line = 5) }
      )
    }
    
    good3(mtcars, "whatever")
    ls()
    

    4) trace A different approach is to to insert example_df into bad_function using trace.

    bad_function <- function(){
      plot(example_df)
    }
    par_1 <- par()
    
    good4 <- function(example_df, example_text) {
      withr::with_par(
        new = list(oma = c(rep(2, 4)), cex = 0.55),
        code = {
          trace(bad_function, bquote(example_df <- .(example_df)), print = FALSE) ##
          bad_function()
          untrace(bad_function) ## 
          mtext(example_text, line = 5)
      })
    }
    
    good4(mtcars, "whatever")
    ls()