On 'on.exit()'

Introduction

on.exit() is a base R function which records expressions to evaluate when the current function exits.

I was wanting to use on.exit() in a function that was allocating some resources, and I explicitly wanted these resources freed in the reverse order.

Working out how to do what I wanted was easy (discussed below), but it lead me down a path of discovery as I tried to figure out just exactly where and when the on.exit() expressions were being executed.

Below are some of my code snippets from this exploration.

Simple usage

The following simple code shows:

  • The on.exit() call can appear anywhere in the function
  • The on.exit() expression is evaluated after the function budy is complete i.e. after the return statement.
f <- function(a, b) {
  on.exit(print("on.exit() called"))
  print("Function finished, returning value")
  return(a + b)
}

f(1, 2)
## [1] "Function finished, returning value"
## [1] "on.exit() called"
## [1] 3

Multiple on.exit() statements

The following code shows:

  • The on.exit() call can appear multiple times in the function
  • by using add = TRUE, the expression is added to any prior expressions
f <- function(a, b) {
  on.exit(print("on.exit() called 1"))
  result <- a + b
  on.exit(print("on.exit() called 2"), add = TRUE)
  print("Function finished, returning value")
  return(result)
}

f(1, 2)
## [1] "Function finished, returning value"
## [1] "on.exit() called 1"
## [1] "on.exit() called 2"
## [1] 3

Multiple on.exit() statements

The following code shows:

  • The on.exit() call can appear multiple times in the function
  • by using add = TRUE, the expression is added to any prior expressions
  • by using after = FALSE, the expression is added before any any prior expressions
f <- function(a, b) {
  on.exit(print("on.exit() called 1"))
  result <- a + b
  on.exit(print("on.exit() called 2"), add = TRUE, after = FALSE)
  print("Function finished, returning value")
  return(result)
}

f(1, 2)
## [1] "Function finished, returning value"
## [1] "on.exit() called 2"
## [1] "on.exit() called 1"
## [1] 3

The evaluation environment for on.exit() has access to function variables

The evaulation environment where the on.exit() expressions are a run has access to variables within the function.

f <- function(a, b) {
  on.exit(cat("The numbers were ", a, " and ", b, "\n"))
  return(a + b)
}

f(1, 2)
## The numbers were  1  and  2
## [1] 3

The variables aren’t explicitly captured when then on.exit() expression is registered, so the variable values will be those at the end of the function evaluation.

f <- function(a, b) {
  on.exit(cat("The numbers were ", a, " and ", b, "\n"))
  a <- a + 1
  b <- b + 1
  return(a + b)
}

f(1, 2)
## The numbers were  2  and  3
## [1] 5

Viewing the expressions registered via on.exit()

It is possible to view the expressions that have been registered with on.exit() during a function call by calling sys.on.exit()

f <- function(a, b) {
  on.exit(print("on.exit() called 1"))
  result <- a + b
  on.exit(print("on.exit() called 2"), add = TRUE, after = FALSE)
  print(sys.on.exit())
  print("Function finished, returning value")
  return(result)
}

f(1, 2)
## {
##     print("on.exit() called 2")
##     print("on.exit() called 1")
## }
## [1] "Function finished, returning value"
## [1] "on.exit() called 2"
## [1] "on.exit() called 1"
## [1] 3

It is even possible to return the on.exit() expressions for later evaluation in a different context.

f <- function(a, b) {
  on.exit(print("on.exit() called 1"))
  result <- a + b
  on.exit(print("on.exit() called 2"), add = TRUE, after = FALSE)
  return(sys.on.exit())
}

# capture the returned on.exit() expressions
exp <- f(1, 2)
## [1] "on.exit() called 2"
## [1] "on.exit() called 1"
exp
## {
##     print("on.exit() called 2")
##     print("on.exit() called 1")
## }
# Evaluate the returned on.exit() expressions
eval(exp)
## [1] "on.exit() called 2"
## [1] "on.exit() called 1"

Can you use on.exit() within an on.exit()? Attempt 1

In the following code, I attempt to ask on.exit() to try and execute another on.exit() during the evaluation of the on.exit() expressions.

This does not work. Only the first on.exit() expression is evaluated.

f <- function() {
  on.exit({print("hello 1")})
  on.exit(on.exit(print('hello 2')), add = TRUE)
}

f()
## [1] "hello 1"

Can you use on.exit() within an on.exit()? Attempt 2

You can sort of make this work by setting up another function context in the on.exit() expression, but nesting on.exit() calls this way doesn’t really do anything that regular usage doesn’t already do.

f <- function() {
  on.exit(print("hello 1"))
  on.exit((function() {on.exit(print("hello 2"))})(), add = TRUE)
}

f()
## [1] "hello 1"
## [1] "hello 2"

Can you use on.exit() within an on.exit()? Attempt 3

It’s interesting to note that on.exit() expressions are evaluated at the end of code created via an eval(parse(...)) statement. i.e.

eval(parse(text="on.exit(print('hello'))"))
## [1] "hello"

So it is possible to get an on.exit() to evaluate within the context of a different on.exit() - but I can’t see an application for this behaviour.

f <- function() {
  on.exit(print("hello 1"))
  on.exit(eval(parse(text="on.exit(print('hello 2'))")), add = TRUE)
}

f()
## [1] "hello 1"
## [1] "hello 2"

What happens if you call stop() within an on.exit()?

If an error occurs during the evaluation of the on.exit() expressions, then the return value is never assigned to the variable in the calling environment.

a <- NULL

f <- function() {
  on.exit(stop("error during on.exit()"))
  return(1)
}

a <- f()
## Error in f(): error during on.exit()
print(a)
## NULL

What happens if you call return() within an on.exit()?

If return() is called within on.exit() then this will actually override any return value in the function itself.

f <- function() {
  on.exit(return(2))
  return(1)
}

f()
## [1] 2

Manipulating the return value with on.exit()

The on.exit() evaluation environment has access to the return value of the function using the returnValue() function.

This can then be used to post-process any return value before it actual gets to the function calling environment.

In the following code, the return value of the function is doubled during the exit process.

f <- function()  {
  on.exit(return(returnValue() * 2))
  return(1)
}

result <- f()
print(result)
## [1] 2

Using Recall() within an on.exit()

In the following code, the return value is evaluated within the on.exit() context, and if it does not meet the specified criteria, the function is called again (via Recall()) until it passes.

set.seed(1)

f <- function() {
  
  on.exit({
    if (returnValue() < 0.9) {
      return(Recall())
    }
    return(x)
  })
  
  x <- runif(1)
  cat("Returning: ", x, "\n")
  return(x)
}


result <- f()
## Returning:  0.2655087 
## Returning:  0.3721239 
## Returning:  0.5728534 
## Returning:  0.9082078
print(result)
## [1] 0.9082078

Shift the entire function body to be evaluated on.exit()

In this code, the entire function body and return statement for this simpl addition function are within the on.exit() statement.

f <- function(a, b) {
  on.exit({
    result <- a + b
    return(result)
  })
  
  
  return(-999)
}



result <- f(1, 2)
print(result)
## [1] 3