#' Multiply two numbers
#'
#' @param x num, a number
#' @param y num, another number
#' @return the product of x and y
= function(x = 2, y = 3){
multiply return(x * y)
}
Try
It’s not whether you get knocked down, it’s whether you get up.
~ Vince Lombardi
Even the best planned functions, processes or iterators will sometimes fail. Well written code will account for this, and most computer languages come with a host of utilities for dealing with failures gracefully. R is not exception with two tools built into the base
package: try()
and tryCatch()
. We won’t investigate tryCatch()
here, but there is a lot written about it. Try is a simple version and will handle the vast majority of common situations.
try()
is a wrapper which is just like a wrapper around a gift - it is light weight and shouldn’t effect the workings of the inner function(s) it wraps. try()
will return one of 2 values: an error-free result or a try-error
class object that holds an error message.
no error whatever the inner function(s) is/are supposed to return, or
error a
try-error
class object (a character with attributes).
The coder (that’s you!) is tasked with testing the value returned, and then decide what best to do if an error occurs.
A simple example function
Let’s write a function that accepts two numeric arguments and multiplies them.
Running without a try()
wrapper
Now let’s try the function - once where it works and once where it doesn’t.
= multiply(x = 2, y = 10)
result_ok cat("result_ok is", result_ok, "\n")
result_ok is 20
#result_not_ok = multiply(x = 4, y = "fooey")
#cat("result_not_ok is", result_not_ok, "\n")
The value of result_not_ok
never gets assigned because an error occured inside the function. So the computer stops. That’s why we had to comment it out - so we could run the code on this page.
Running with a try()
wrapper.
Let’s try again, but this time wrap each in a try()
function.
= try( multiply(x = 5, y = 2) )
result_ok cat("result_ok is", result_ok, "\n")
result_ok is 10
= try( multiply(x = 6, y = "dog") ) result_not_ok
Error in x * y : non-numeric argument to binary operator
cat("result_not_ok is", result_not_ok, "\n")
result_not_ok is Error in x * y : non-numeric argument to binary operator
So the error occurs inside the function but note that result_not_ok
does get assigned some kind of value… the error message. So while an error occured, the code continued to flow.
Investigate the result of try()
Let’s print the result_not_ok
object outside of the call to cat()
which does pretty printing. We want to see the raw data itself.
result_not_ok
[1] "Error in x * y : non-numeric argument to binary operator\n"
attr(,"class")
[1] "try-error"
attr(,"condition")
<simpleError in x * y: non-numeric argument to binary operator>
So it is a single element character vector (in this case) with two attributes: “class” and “condition” where “condition” is a two element list of “message” and “call”. The condition is set by the inner function and can include many details, but in this case it contains just the default message generated by the *
function (yep, *
is a function but that is another story.)
Test the result for it’s class
Let’s test the class of the result - that is see if it inherits from the try-error
, and then decide what to do.
= try( multiply(x = 5, y = 2) )
result_ok if (inherits(result_ok, "try-error")){
cat("Oopsie! An error occured!\n")
cat("result_ok was assigned a try_error class object\n")
cat("Here's the message:", attr(result_ok, "condition")$message, "\n")
else {
} cat("result_ok is", result_ok, "\n")
}
result_ok is 10
= try( multiply(x = 6, y = "dog") ) result_not_ok
Error in x * y : non-numeric argument to binary operator
if (inherits(result_not_ok, "try-error")){
cat("Oopsie! An error occured!\n")
cat("result_not_ok was assigned a try_error class object\n")
cat("Here's the message:", attr(result_not_ok, "condition")$message, "\n")
else {
} cat("result_ok is", result_not_ok, "\n")
}
Oopsie! An error occured!
result_not_ok was assigned a try_error class object
Here's the message: non-numeric argument to binary operator
An example with lapply()
try()
really shines when you are iterating over a list of items. It’s awful when one iteration throws an error because the iterator simply stops. So, let’s use try()
to allow the iterator to complete it’s loops.
First let’s define x
and y
with 5 elements each. x
is an atomic (low level) vector of all the same data type, but y` is a list so it can have mixed data types.
= 5:9
x = list(10, 11, "cat", 13, 14) y
Now let’s iterate over a sequence of indices into x
and y
. We can use seq_along(x))
which yields {r}seq_along(x))
.
= lapply(seq_along(x),
r function(i){
cat("iteration number", i, "\n")
= try(multiply(x[[i]], y[[i]]))
value if (inherits(value, 'try-error')){
= NULL
value
}return(value)
})
iteration number 1
iteration number 2
iteration number 3
Error in x * y : non-numeric argument to binary operator
iteration number 4
iteration number 5
r
[[1]]
[1] 50
[[2]]
[1] 66
[[3]]
NULL
[[4]]
[1] 104
[[5]]
[1] 126
Here you can see that the iterator successfully completed all of the loops even though it complained about the third iteration, and that it returned NULL
in when i == 3
. No crash! Of course, further decisions must be made about what to do with the NULL
, but at least the progam continues.
Using try()
iterating over a table.
Following the idea of iterating over rows, or groups of rows, of a table, we can implement the try()
in each iteration. Instead of groups of rows (based upon some value in a column), here we simply want to iterate over each row. First we make the table (data.frame, tibble, etc).
library(dplyr)
Attaching package: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
= dplyr::tibble(x = x, y = y)
tab tab
# A tibble: 5 × 2
x y
<int> <list>
1 5 <dbl [1]>
2 6 <dbl [1]>
3 7 <chr [1]>
4 8 <dbl [1]>
5 9 <dbl [1]>
Next we declare not groupings but instead flag the table to be treated row-wise. Then we declare an anonymous function that accepts one row of the table and adds the product of x and y. Since we are iterating over the rows the key
value will not have any content (an empty table) but the tbl
value will contain the complete row of data.
= tab |>
result ::rowwise() |>
dplyr::group_map(
dplyrfunction(tbl, key){
= try(multiply(tbl$x[[1]], tbl$y[[1]]))
z if (inherits(z, "try-error")){
= NULL
tbl else {
} = tbl |>
tbl ::mutate(z = z)
dplyr
}return(tbl)
})
Error in x * y : non-numeric argument to binary operator
result
[[1]]
# A tibble: 1 × 3
x y z
<int> <list> <dbl>
1 5 <dbl [1]> 50
[[2]]
# A tibble: 1 × 3
x y z
<int> <list> <dbl>
1 6 <dbl [1]> 66
[[3]]
NULL
[[4]]
# A tibble: 1 × 3
x y z
<int> <list> <dbl>
1 8 <dbl [1]> 104
[[5]]
# A tibble: 1 × 3
x y z
<int> <list> <dbl>
1 9 <dbl [1]> 126
Note that by catching the error, we allowed the iterator to complete its loops while returnin NULL
as the value of the third element. If we row-bind these together, R will drop the NULL
result when forming the merged table.
|>
result ::bind_rows() |>
dplyrprint()
# A tibble: 4 × 3
x y z
<int> <list> <dbl>
1 5 <dbl [1]> 50
2 6 <dbl [1]> 66
3 8 <dbl [1]> 104
4 9 <dbl [1]> 126