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)
}
)
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
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()