Last week we learned “reactive” strategies, to fix bugs. Why not be “proactive”: write a bit of extra code now, to check that inputs/relevant variables are in order, to prevent bugs later? This is the idea behind defensive programming
Main principle: “fail fast”, i.e., to terminate whenever something appears to be wrong and may cause bugs downstream
(Editorial comment: not everyone is a proponent of defensive programming; there are some downsides, but it’s a good thing to know about, and use in moderation. And it’s very relevant when we’re writing functions to read in/deal with data, our focus the rest of this week!)
add.up.inv.powers = function(n) {
return(sum((1:n)^(1/1:n)))
}
add.up.inv.powers(10) # Working well
## [1] 13.15116
add.up.inv.powers(-10) # Uh oh! Why NaN?
## [1] NaN
add.up.inv.powers("R") # Oh boy, error
## Warning in add.up.inv.powers("R"): NAs introduced by coercion
## Error in 1:n: NA/NaN argument
stop()
, warning()
The functions stop()
and warning()
provide the most basic mechanism for error checking in R. They can be used inside a function to throw an error and a warning, respectively. As in:
my.fun = function(arg1, arg2, arg3) {
# Perhaps some initial code
if (cond) { # cond is some condition we're checking
stop("Oh no! The condition is not true!")
}
# Some further code here
}
Note: throwing an error terminates the function, but throwing a warning does not
Let’s add in some functionality to check whether the input is a positive integer, and if not, throw an error
add.up.inv.powers = function(n) {
# Check whether n is a positive integer, otherwise throw an error
# (Note the lazy OR, only if n==is.integer(n) does n <= 0 get evaluated)
if (n != as.integer(n) || n <= 0) stop("n must be a positive integer")
return(sum((1:n)^(1/1:n)))
}
add.up.inv.powers(10) # Working well
## [1] 13.15116
add.up.inv.powers(-10) # Yay! We caught this
## Error in add.up.inv.powers(-10): n must be a positive integer
add.up.inv.powers("R") # Still an error!
## Warning in add.up.inv.powers("R"): NAs introduced by coercion
## Error in if (n != as.integer(n) || n <= 0) stop("n must be a positive integer"): missing value where TRUE/FALSE needed
Error checking is not always trivial! The error we got in the last call was because we were were trying to compare n
to as.integer(n)
when n
was a string (“R”, in the last function call), and as.integer(n)
was returning NA
The fix: use another lazy OR at the front, to check whether we have a numeric variable
add.up.inv.powers = function(n) {
# Check whether n is a positive integer, otherwise throw an error
# (Note the lazy ORs below)
if (!is.numeric(n) || n != as.integer(n) || n <= 0) {
stop("n must be a positive integer")
}
return(sum((1:n)^(1/1:n)))
}
add.up.inv.powers(10) # Working well
## [1] 13.15116
add.up.inv.powers(-10) # Yay! We caught this
## Error in add.up.inv.powers(-10): n must be a positive integer
add.up.inv.powers("R") # Yay! We caught this
## Error in add.up.inv.powers("R"): n must be a positive integer
Finally, depending on what we intend to use this function for, we might just throw a warning instead of an error, and return NA (or some default value) to signal that we couldn’t compute the expression
add.up.inv.powers = function(n) {
# Check whether n is a positive integer, otherwise throw an error
# (Note the lazy ORs below)
if (!is.numeric(n) || n != as.integer(n) || n <= 0) {
warning("n must be a positive integer; returning NA")
return(NA)
}
return(sum((1:n)^(1/1:n)))
}
add.up.inv.powers(10) # Working well
## [1] 13.15116
add.up.inv.powers(-10) # Yay! We caught this
## Warning in add.up.inv.powers(-10): n must be a positive integer; returning
## NA
## [1] NA
add.up.inv.powers("R") # Yay! We caught this
## Warning in add.up.inv.powers("R"): n must be a positive integer; returning
## NA
## [1] NA
tryCatch()
The function tryCatch()
allows us to do error handling in R. We can use it to try to run an expression, and if we encounter any error then we can catch it, i.e., we can handle the error in a custom way. As in:
tryCatch({
# Some code goes here that could possibly throw errors
}, error = function(e) {
# Some code goes here for dealing with the error e
return(NULL) # Customary; we could also return NA
})
summarize.some.cols = function(mat, col.inds) {
out = tryCatch({
apply(mat[,col.inds,drop=FALSE], MARGIN=2, FUN=summary)
}, error = function(e) {
cat("Encountered the following error:\n")
cat("\t", e$message, "\n")
cat("Returning NULL")
return(NULL)
})
# If out is NULL, then we must have run the error function, so let's
# just return NULL
if (is.null(out)) return(NULL)
# If we get down here, then the apply statement succeeded. Let's assign
# some column names before returning
colnames(out) = paste("Col", col.inds)
return(out)
}
mat = matrix(rnorm(16), 4, 4)
summarize.some.cols(mat, 1:2) # Works
## Col 1 Col 2
## Min. -2.6460 -0.1936
## 1st Qu. -2.2450 0.1505
## Median -1.1250 0.4645
## Mean -1.2500 0.8282
## 3rd Qu. -0.1310 1.1420
## Max. -0.1052 2.5770
summarize.some.cols(mat, 3) # Works
## Col 3
## Min. -1.6000
## 1st Qu. -1.3840
## Median -0.6767
## Mean -0.3420
## 3rd Qu. 0.3655
## Max. 1.5860
summarize.some.cols(mat, 3:7) # Caught an error!
## Encountered the following error:
## subscript out of bounds
## Returning NULL
## NULL