5 mins read

R: from on.exit() to quasiquotation

Category: R

Part 1

Often one needs to write functions that not only perform computation on its own, but also interact with the world outside of the function execution environment, for example, writing to a file, saving plots or changing working directory. Computer scientists called these operations side effects, in the sense that these functions change some aspects of the global state of the software. To be more precise, writing to a file requires a file connection to be established, saving plots requires opening a graphical device and changing working directories affects how to find things in the computer file system. In these cases, we usually want to make sure that when the function exits, whether normally of because of an error, the global state is restored, so that other functions that depends on the global state won’t be affected.

on.exit function comes to the rescue: it captures the expression you supplied and run that expression when the function exits, whether normally or because of error. To see this in action, suppose we need to change the digits option to 3 during the function execution. To make sure the original option is restored on exit of the function, we can do this:

f <- function() {
  old <- options("digits") # save away original options
  options(digits = 3) # change the option
  on.exit(options(old)) # register a function to restore the 
                        # original option before the function exit

  print(options("digits"))
  cat("exiting...\n\n")
}
options("digits")
## $digits
## [1] 7
f()
## $digits
## [1] 3
## 
## exiting...
options("digits")
## $digits
## [1] 7

So we see that the expression that we supplied to on.exit, options(old), got execuated right before the function exited (i.e., after all codes of the function finished execution, but before the function returns), so that the option digits is restored to the original value, 7. To test that the option gets restored even if an error is encountered:

f <- function() {
  old <- options("digits")
  options(digits = 3)
  on.exit(options(old))

  print(options("digits"))
  stop("unexpected error...")
  
  cat("exiting...\n")
}
options("digits")
# $digits
# [1] 3

f()
# $digits
# [1] 3

# Error in f() : unexpected error...

options("digits")
# $digits
# [1] 7

This time, the function exits immediately when it sees the error, so cat(exiting...\n) was not run. However, on.exit(options(old)) was executed before the error, which means the expression options(old) had already been registred and the digits option got restored on exit. Note that this does mean that we need to make sure that errors can not possibly occur between changing options (options(digits = 3)) and registering the function to restore original option (on.exit(options(old))). So perhaps it’s a good habit to follow an on.exit statement immediately after changing global state (see shiny::runApp for numerous examples).

Part 2

sys.on.exit is a useful function that allows us to peek into what’s registered at any point in time:

test.sys.on.exit <- function() {
  on.exit(print(1))
  ex <- sys.on.exit()
  print(ex)
  cat("exiting...\n")
}

test.sys.on.exit()
## print(1)
## exiting...
## [1] 1

We can see that sys.on.exit returns print(1) which is waiting to be executed before the function returns.

Part 3

At the begining of part 1, I used the bold capture because on.exit() is implemented with one of the most interesting feature of R: computing on language, or non-standard evaluation, which basically means that R has the ability to capture literally what you typed and hold an unevaluated abstract syntax tree, instead of evaluating what you typed.

Let’s see what will happen if we put on.exit in a loop:

test <- function() {
  for (i in 1:3) {
    on.exit(print(i), add = TRUE)
    ex <- sys.on.exit()
    print(ex)
  }
  cat("exiting...\n")
}

test()
## print(i)
## {
##     print(i)
##     print(i)
## }
## {
##     print(i)
##     print(i)
##     print(i)
## }
## exiting...
## [1] 3
## [1] 3
## [1] 3

We see that on.exit does not do any evaluation at all. By the time the function exits, the loop has already finished and the value of i is 3. So when R evaluates print(i) 3 times, it prints 3 three times. What if I want to capture the value of i at the time when I register the expression? According to the help page:

to capture the current values in expr use substititute or similar.

So, with my newly acquired knowledge of NSE, I tried using substitue and eval:

test <- function() {
  for (i in 1:3) {
    # use subsitite to subsititute in the value of i
    quoted <- substitute(on.exit(expr = print(x), add = TRUE), list(x = i))
    print(quoted)
    eval(quoted)
  }
  cat("exiting...\n")
}

test()
## on.exit(expr = print(1L), add = TRUE)
## [1] 1
## on.exit(expr = print(2L), add = TRUE)
## [1] 2
## on.exit(expr = print(3L), add = TRUE)
## [1] 3
## exiting...

Hmmm, it seems that eval and substitute did work in the sense that they inlined 1L, 2L and 3L into print, however, two strange things happened:

  • first, the eval call actually printed i out during the execution of the loop while it is supposed to register the print calls on exit without evaluating them,
  • second, the print calls are not registered on exit…

Why? I don’t know… However, do.call actually works:

test <- function() {
  for (i in 1:3) {
    do.call("on.exit", 
            list(expr = substitute(expr = print(x), list(x = i)),
                 add = TRUE))
  }
  cat("exiting...\n")
}

test()
## exiting...
## [1] 1
## [1] 2
## [1] 3

This really puzzles me and I feel maybe it’s some undocumentated behavior of on.exit. on.exit is a function that automatically quote the argument, that is, it doesn’t evaluate the argument, instead, it captures the argument as an unevalutated abstract syntax tree. For such base R functions, Hadley discussed four forms of unquotation. Unfortunately, on.exit doesn’t fall into any of those four forms and it doesn’t provide users with any facility to escape the automatic quoting. This makes me to appreciate quasiquotation - If base R had quasiquotation, it’d be much easier to evaluate part of the expression and I could write something like this:

test <- function() {
  for (i in 1:3) {
    # !! for unquote part of an expression, taken from tidyverse
    on.exit(print(!!i), add = TRUE)
  }
  cat("exiting...\n")
}

And yes, if anyone figured out why on earth eval didn’t work but do.call worked (and why eval printed i), please let me know!