Issue
Given a regular R function f
, I’d like to be able to create a new function f_debug
that acts just like f
, but lets me keep track of all the assignments to function-local variables that happened inside it.
For example:
f <- function(x, y) {
z <- x + y
df <- data.frame(z=z)
df
}
# This function doesn't work as intended - would like it to (in the case of `f` above)
# write out a list containing `z` and `df` to an RDS file
capturing <- function(func) {
e <- new.env()
altered <- function(...) {
parent <- parent.frame()
e <- something...(func, environment(), parent, etc., etc.)
result <- func(...)
saveRDS(as.list(e), 'foo.rds')
result
}
environment(func) <- e
altered
}
f_debug <- capturing(f)
I’m not sure whether my knowledge gap to do this is large or small, anyone have a solution?
Solution
What you are describing is already implemented in base R with utils::dump.frames
, in an even more sophisticated way. It saves the frame (environment) associated with each call in the call stack to an object of class "dump.frames"
, which you can explore retroactively with utils::debugger
as if you had actually run your code under a debugger.
capturing <- function(func, ...) {
cc <- as.call(c(quote(utils::dump.frames), list(...)))
cc <- call("on.exit", cc, add = TRUE)
body(func) <- call("{", cc, body(func))
func
}
capturing
injects the call on.exit(utils::dump.frames(...), add = TRUE)
into the body of func
and returns the modified function.
Here, ...
is a list of arguments to dump.frames
:
dumpto
, a character string giving the name to be used for the"dump.frames"
objectto.file
, a logical flag indicating whether the"dump.frames"
object should be assigned in the global environment orsave
-ed topaste0(dumpto, ".rda")
in the current working directoryinclude.GlobalEnv
, a logical flag indicating whether the global environment should be saved as well
A quick example, which you should try yourself:
tmp <- tempfile()
dir.create(tmp)
cwd <- setwd(tmp)
f <- function(x, y) {
z <- x + y
z + 1
}
g <- capturing(f, dumpto = "zzz", to.file = TRUE)
h <- function(a, b) {
d <- g(a, b)
d + 1
}
h12 <- h(1, 2)
load("zzz.rda")
zzz
## $`h(1, 2)`
## <environment: 0x14c16cb58>
##
## $`#2: g(a, b)`
## <environment: 0x14c16ca40>
##
## attr(,"error.message")
## [1] ""
## attr(,"class")
## [1] "dump.frames"
ls(zzz[[1L]])
## [1] "a" "b"
ls(zzz[[2L]])
## [1] "z" "x" "y"
utils::debugger(zzz)
## Message: Available environments had calls:
## 1: h(1, 2)
## 2: #2: g(a, b)
##
## Enter an environment number, or 0 to exit
## Selection: 2
## Browsing in the environment with call:
## #2: g(a, b)
## Called from: debugger.look(ind)
## Browse[1]> ls()
## [1] "x" "y" "z"
## Browse[1]> x == 1 && y == 2 && z == x + y
## [1] TRUE
## Browse[1]> Q
setwd(cwd)
unlink(tmp, recursive = TRUE)
See ?browser
if you are unfamiliar with R’s environment browser.
My capturing
function has the limitation that on.exit
calls in the body of func
must also use add = TRUE
. If you have written func
yourself, then it is not much of a limitation at all, and passing add = TRUE
is a good habit anyway.
Ultimately, there is no completely safe way to inject code into functions, but, in an interactive setting, I would say that this level of "unsafety" is fine.
Answered By – Mikael Jagan
Answer Checked By – Mildred Charles (BugsFixing Admin)