views:

110

answers:

2

I want to create a function that will retry an expression if it fails. Here's my working version:

retry <- function(.FUN, max.attempts=3, sleep.seconds=1) {
  x <- NULL
  if(max.attempts > 0) {
    f <- substitute(.FUN)
    x <- try(eval(f))
    if(class(x) == "try-error") {
      Sys.sleep(sleep.seconds)
      return(suppressWarnings(retry(.FUN, max.attempts-1)))
    }
  }
  x
}

retry(stop("I'm here"))

If I remove the suppressWarnings() function above, then I get a set of warnings on each recursive call. Does anyone know what I'm doing wrong that would cause that?

Here's an example that can be run repeatedly:

retry({ tmp <- function() { if(rnorm(1) < 0) stop("I'm here") else "success" }; tmp() })
+2  A: 

Not sure about why you get the warnings... but if use a for loop they disappear.

retry <- function(.FUN, max.attempts=3, sleep.seconds=1) 
  {
  x <- NULL
  for (i in 1:max.attempts)
      {
      f <- substitute(.FUN)
      x <- try(eval(f))
      if (class(x) == "try-error")
         {
         Sys.sleep(sleep.seconds)
         }
       else
         {
         return (x)
         }
      }
  x
  }
nico
Thanks @nico; I was mostly curious about what was causing the warnings with substitute. But your version works perfectly!
Shane
+4  A: 

I'm not sure if I can describe the cause exactly, but I've isolated the problem and can fix it. The basic problem is the recursion: retry(.FUN, max.attempts-1) - when the recursive call calls substitute(.FUN) it's going to have go up a level of the call stack to figure out what the value of .FUN is - it has to restart the evaluation of a promise (the delayed execution of function arguments) a level up.

A fix is to just do the substitution once:

retry <- function(.FUN, max.attempts = 3, sleep.seconds = 0.5) {
  expr <- substitute(.FUN)
  retry_expr(expr, max.attempts, sleep.seconds)
}

retry_expr <- function(expr, max.attempts = 3, sleep.seconds = 0.5) {
  x <- try(eval(expr))

  if(inherits(x, "try-error") && max.attempts > 0) {
    Sys.sleep(sleep.seconds)
    return(retry_expr(expr, max.attempts - 1))
  }

  x
}

f <- function() {
  x <- runif(1)
  if (x < 0.5) stop("Error!") else x
}

retry(f())

To create functions that you can use flexibly, I highly recommend minimising the use of substitute. In my experience, you're usually best off having one function that does the substitution, and another that does all the work. This makes it possible to use the function when called from another function:

g1 <- function(fun) {
  message("Function starts")
  x <- retry(fun)
  message("Function ends")
  x
}
g1(f())
# Function starts
# Error in eval(expr, envir, enclos) : object 'fun' not found
# Error in eval(expr, envir, enclos) : object 'fun' not found
# Error in eval(expr, envir, enclos) : object 'fun' not found
# Error in eval(expr, envir, enclos) : object 'fun' not found
# Function ends

g2 <- function(fun) {
  message("Function starts")
  expr <- substitute(fun)
  x <- retry_expr(expr)
  message("Function ends")
  x
}
g2(f())
# Function starts
# Error in f() : Error!
# Function ends
# [1] 0.8079241
hadley
I would have thought that the recursive execution of .FUN in your version wouldn't work because .FUN would have already been evaluated at that point? I'll test it...
Shane
I think you're right, but in the meantime I figured it out. I think my f is a better example because sometimes it errors and sometimes it doesn't. Run it a few times to check that it does what you expect. I'm not sure what you wanted it to return when you run out of attempts but still have an error.
hadley
Oh, I see you had an equivalent to my f in the bottom of your post :/
hadley
Fantastic! Thanks! I had a feeling it had something to do with the levels in the stack because I could see the number of warnings grew on each iteration.
Shane
And that modification also makes it possible to use `retry` from within another function. See addendum at bottom.
hadley