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
usesubstititute
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 theprint
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!