6.7 Exiting a function
Most functions exit in one of two ways29: they either return a value, indicating success, or they throw an error, indicating failure. This section describes return values (implicit versus explicit; visible versus invisible), briefly discusses errors, and introduces exit handlers, which allow you to run code when a function exits.
6.7.1 Implicit versus explicit returns
There are two ways that a function can return a value:
Implicitly, where the last evaluated expression is the return value:
function(x) { j01 <-if (x < 10) { 0 else { } 10 } }j01(5) #> [1] 0 j01(15) #> [1] 10
Explicitly, by calling
return()
:function(x) { j02 <-if (x < 10) { return(0) else { } return(10) } }
6.7.2 Invisible values
Most functions return visibly: calling the function in an interactive context prints the result.
function() 1
j03 <-j03()
#> [1] 1
However, you can prevent automatic printing by applying invisible()
to the last value:
function() invisible(1)
j04 <-j04()
To verify that this value does indeed exist, you can explicitly print it or wrap it in parentheses:
print(j04())
#> [1] 1
j04())
(#> [1] 1
Alternatively, you can use withVisible()
to return the value and a visibility flag:
str(withVisible(j04()))
#> List of 2
#> $ value : num 1
#> $ visible: logi FALSE
The most common function that returns invisibly is <-
:
2
a <- 2)
(a <-#> [1] 2
This is what makes it possible to chain assignments:
b <- c <- d <- 2 a <-
In general, any function called primarily for a side effect (like <-
, print()
, or plot()
) should return an invisible value (typically the value of the first argument).
6.7.3 Errors
If a function cannot complete its assigned task, it should throw an error with stop()
, which immediately terminates the execution of the function.
function() {
j05 <-stop("I'm an error")
return(10)
}j05()
#> Error in j05(): I'm an error
An error indicates that something has gone wrong, and forces the user to deal with the problem. Some languages (like C, Go, and Rust) rely on special return values to indicate problems, but in R you should always throw an error. You’ll learn more about errors, and how to handle them, in Chapter 8.
6.7.4 Exit handlers
Sometimes a function needs to make temporary changes to the global state. But having to cleanup those changes can be painful (what happens if there’s an error?). To ensure that these changes are undone and that the global state is restored no matter how a function exits, use on.exit()
to set up an exit handler. The following simple example shows that the exit handler is run regardless of whether the function exits normally or with an error.
function(x) {
j06 <-cat("Hello\n")
on.exit(cat("Goodbye!\n"), add = TRUE)
if (x) {
return(10)
else {
} stop("Error")
}
}
j06(TRUE)
#> Hello
#> Goodbye!
#> [1] 10
j06(FALSE)
#> Hello
#> Error in j06(FALSE): Error
#> Goodbye!
on.exit()
is useful because it allows you to place clean-up code directly next to the code that requires clean-up:
function(dir, code) {
cleanup <- setwd(dir)
old_dir <-on.exit(setwd(old_dir), add = TRUE)
options(stringsAsFactors = FALSE)
old_opt <-on.exit(options(old_opt), add = TRUE)
}
Coupled with lazy evaluation, this creates a very useful pattern for running a block of code in an altered environment:
function(dir, code) {
with_dir <- setwd(dir)
old <-on.exit(setwd(old), add = TRUE)
force(code)
}
getwd()
#> [1] "/root"
with_dir("~", getwd())
#> [1] "/root"
The use of force()
isn’t strictly necessary here as simply referring to code
will force its evaluation. However, using force()
makes it very clear that we are deliberately forcing the execution. You’ll learn other uses of force()
in Chapter 10.
The withr package (Hester et al. 2018) provides a collection of other functions for setting up a temporary state.
In R 3.4 and earlier, on.exit()
expressions are always run in order of creation:
function() {
j08 <-on.exit(message("a"), add = TRUE)
on.exit(message("b"), add = TRUE)
}j08()
#> a
#> b
This can make cleanup a little tricky if some actions need to happen in a specific order; typically you want the most recent added expression to be run first. In R 3.5 and later, you can control this by setting after = FALSE
:
function() {
j09 <-on.exit(message("a"), add = TRUE, after = FALSE)
on.exit(message("b"), add = TRUE, after = FALSE)
}j09()
#> b
#> a
6.7.5 Exercises
What does
load()
return? Why don’t you normally see these values?What does
write.table()
return? What would be more useful?How does the
chdir
parameter ofsource()
compare towith_dir()
? Why might you prefer one to the other?Write a function that opens a graphics device, runs the supplied code, and closes the graphics device (always, regardless of whether or not the plotting code works).
We can use
on.exit()
to implement a simple version ofcapture.output()
.function(code) { capture.output2 <- tempfile() temp <-on.exit(file.remove(temp), add = TRUE, after = TRUE) sink(temp) on.exit(sink(), add = TRUE, after = TRUE) force(code) readLines(temp) }capture.output2(cat("a", "b", "c", sep = "\n")) #> [1] "a" "b" "c"
Compare
capture.output()
tocapture.output2()
. How do the functions differ? What features have I removed to make the key ideas easier to see? How have I rewritten the key ideas so they’re easier to understand?
References
Hester, Jim, Kirill Müller, Kevin Ushey, Hadley Wickham, and Winston Chang. 2018. Withr: Run Code with Temporarily Modified Global State. http://withr.r-lib.org.
Functions can exit in other more esoteric ways like signalling a condition that is caught by an exit handler, invoking a restart, or pressing “Q” in an interactive browser.↩︎