Search code examples
rdebuggingtrace

Use `trace` to skip a line in an R function


Assume I have a function where I want to tweak the behavior a little bit (skip a certain line, say). I could

  1. Copy & paste the function and change it.
  2. Use debug to change the code interactively.
  3. Use trace to inject own code.

I am only interested in Option 3. for this post.

For instance, let's assume the following very simple code:

f <- function(dbg) {
  x <- rnorm(1)
  if (dbg) {
     cat("Message 1\n")
  }
  if (x < 0 & dbg) {
     cat("Message 2\n")
  } 
  x
}

If (for whatever reason) I want to skip Message 2 (but keep Message 1) I could use trace as follows:

set.seed(12231)
untrace(f)
f(TRUE)
# Message 1
# Message 2
# [1] -0.5787277

trace(f, quote(dbg <- FALSE), at = 4, print = FALSE)
set.seed(12231)
f(TRUE)
# Message 1
# [1] -0.5787277

Now assume a slightly different function g:

g <- function() {
  x <- rnorm(1)
  cat("Message\n")
  cat("Message 2\n")
  x
}

Conceptually I would like to do something like:

trace(g, quote(if(FALSE)), at = 4, print = FALSE) 

but this does not work obviously.

Is there a way how I could use trace to skip (a) certain line(s)? Or is my only option to copy, paste and edit the function?


Solution

  • Effectively all trace does (for regular functions) is insert a call to .doTrace at the desired point inside the function body. We can do the same manually to achieve the exact same effect.

    But in your case we want to remove instead of add; and we can do this as well:

    modified_body <- body(g)
    modified_body[4] <- NULL
    
    original_body <- body(g)
    body(g) <- modified_body
    
    g()
    
    # “untrace”:
    body(g) <- original_body
    

    I tried getting this to work with trace, by passing the modified body as the tracer. Unfortunately I cannot prevent the original function from executing afterwards: adding an explicit return() call to the tracer does nothing (since .doTrace uses eval.parent to evaluate the tracer code, and return inside eval does not exit the calling scope). I can use stop() instead of return(). However, this is undesirable since it always drops to the top level rather than to the caller of g. I tried to influence this by installing a globalCallingHandler but there is no appropriate restart I could invoke, and at any rate regular restarts would drop us right back into g rather than its caller.

    I don’t know if this could be circumvented even in principle: it would essentially require modifying the active call stack — which restarts do, but only in very limited ways as far as I know (namely, by dropping the entire call stack via the abort restart, or by resuming inside the innermost call frame).