A Friendlier Condition Handler for R, inspired by {purrr} mappers and based on {rlang}.
{attempt} is designed to handle the cases when something / someone attempts to do something it shouldn’t.
For example :
log("a")
(error)paste()
"good morning
and iris
(message/warning){attempt} provides several condition handlers, from try catch to simple message printing.
{attempt} only depends on {rlang}, making it easy to implement in other functions and packages.
attempt
is a wrapper around base try
that allows you to insert a custom messsage on error.
library(attempt)
attempt(log("a"))
# Error: argument non numérique pour une fonction mathématique
attempt(log("a"), msg = "Nop !")
# Error: Nop !
You can make it verbose (i.e. returning the expression):
Of course the result is returned if there is one:
As with try
, the result cant be saved as an error object :
a <- attempt(log("a"), msg = "Nop !", verbose = TRUE)
a
#> [1] "Error in log(\"a\"): Nop !\n"
#> attr(,"class")
#> [1] "try-error"
#> attr(,"condition")
#> <simpleError in log("a"): Nop !>
# [1] "Error in log(\"a\"): Nop !\n"
# attr(,"class")
# [1] "try-error"
# attr(,"condition")
# <simpleError in log("a"): Nop !>
silent_attempt
is a wrapper around silently
(see further down for more info) and attempt
. It attempts to run the expr, stays silent if the expression succeeds, and returns error or warnings if any.
You can write a try catch with these params :
expr
the expression to be evaluated.e
a one side formula or a function evaluated when an error occurs.w
a one side formula or a function evaluated when a warning occurs.f
a one side formula or an expression which is always evaluated before returning or exitingtry_catch(log("a"),
.e = ~ paste0("There is an error: ", .x),
.w = ~ paste0("This is a warning: ", .x))
#> [1] "There is an error: Error in log(\"a\"): argument non numérique pour une fonction mathématique\n"
#[1] "There is an error: Error in log(\"a\"): argument non numérique pour une fonction mathématique\n"
try_catch(log("a"),
.e = ~ stop(.x),
.w = ~ warning(.x))
#> Error in log("a"): argument non numérique pour une fonction mathématique
# Error in log("a") : argument non numérique pour une fonction mathématique
try_catch(matrix(1:3, nrow= 2),
.e = ~ print(.x),
.w = ~ print(.x))
#> <simpleWarning in matrix(1:3, nrow = 2): la longueur des données [3] n'est pas un diviseur ni un multiple du nombre de lignes [2]>
#<simpleWarning in matrix(1:3, nrow = 2): la longueur des données [3] n'est pas un diviseur ni un multiple du nombre de lignes [2]>
try_catch(2 + 2 ,
.f = ~ print("Using R for addition... ok I'm out!"))
#> [1] 4
# [1] "Using R for addition... ok I'm out!"
# [1] 4
As usual, the handlers are set only if you call them :
try_catch(matrix(1:3, nrow = 2), .e = ~ print("error"))
#> Warning in matrix(1:3, nrow = 2): la longueur des données [3] n'est pas un
#> diviseur ni un multiple du nombre de lignes [2]
#> [,1] [,2]
#> [1,] 1 3
#> [2,] 2 1
# [,1] [,2]
# [1,] 1 3
# [2,] 2 1
# Warning message:
# In matrix(1:3, nrow = 2) :
# la longueur des données [3] n'est pas un diviseur ni un multiple du nombre de lignes [2]
{attempt} is flexible in how you can specify your arguments.
You can, as you do with {base} tryCatch
, use a plain old function:
try_catch(log("a"),
.e = function(e){
print(paste0("There is an error: ", e))
print("Ok, let's save this")
time <- Sys.time()
a <- paste("+ At",time, ", \nError:",e)
# write(a, "log.txt", append = TRUE) # commented to prevent from log.txt creation on your machine
print(paste("log saved on log.txt at", time))
print("let's move on now")
})
#> [1] "There is an error: Error in log(\"a\"): argument non numérique pour une fonction mathématique\n"
#> [1] "Ok, let's save this"
#> [1] "log saved on log.txt at 2018-01-03 10:08:19"
#> [1] "let's move on now"
# [1] "There is an error: Error in log(\"a\"): argument non numérique pour une fonction mathématique\n"
# [1] "Ok, let's save this"
# [1] "log saved on log.txt at 2017-12-20 18:24:05"
# [1] "let's move on now"
You can even mix both:
try_catch(log("a"),
.e = function(e){
paste0("There is an error: ", e)
},
.f = ~ print("I'm not sure you can do that pal !"))
#> [1] "There is an error: Error in log(\"a\"): argument non numérique pour une fonction mathématique\n"
# [1] "I'm not sure you can do that pal !"
# [1] "There is an error: Error in log(\"a\"): argument non numérique pour une fonction mathématique\n"
try_catch_df
returns a tibble with the call, the error message if any, the warning message if any, and the value of the evaluated expression or “error”. The values will always be contained in a list-column.
res_log <- try_catch_df(log("a"))
res_log
#> call error warning
#> 1 log("a") argument non numérique pour une fonction mathématique NA
#> value
#> 1 error
res_log$value
#> [[1]]
#> [1] "error"
res_matrix <- try_catch_df(matrix(1:3, nrow = 2))
res_matrix
#> call error
#> 1 matrix(1:3, nrow = 2) NA
#> warning
#> 1 la longueur des données [3] n'est pas un diviseur ni un multiple du nombre de lignes [2]
#> value
#> 1 1, 2, 3, 1
res_matrix$value
#> [[1]]
#> [,1] [,2]
#> [1,] 1 3
#> [2,] 2 1
res_success <- try_catch_df(log(1))
res_success
#> call error warning value
#> 1 log(1) NA NA 0
res_success$value
#> [[1]]
#> [1] 0
map_try_catch
and map_try_catch_df
allow you to map on a list of arguments l
, to be evaluated by the function in fun
.
map_try_catch(l = list(1, 3, "a"), fun = log, .e = ~ .x)
#> [[1]]
#> [1] 0
#>
#> [[2]]
#> [1] 1.098612
#>
#> [[3]]
#> <simpleError in .Primitive("log")("a"): argument non numérique pour une fonction mathématique>
map_try_catch_df(list(1,3,"a"), log)
#> call
#> 1 .Primitive("log")(1)
#> 2 .Primitive("log")(3)
#> 3 .Primitive("log")("a")
#> error warning value
#> 1 <NA> NA 0
#> 2 <NA> NA 1.098612
#> 3 argument non numérique pour une fonction mathématique NA error
silently
transforms a function so that when you call this new function, it returns nothing unless there is an error or a warning (contrary to attempt
that returns the result). In a sense, the new function stay silent unless error or warning.
silent_log <- silently(log)
silent_log(1)
silent_log("a")
# Error: argument non numérique pour une fonction mathématique
With silently
, the result is never returned.
surely
also transforms a function so that when you call this new function, it returns calls attempt()
- i.e. in the code below, calling sure_log(1)
is the same as calling attempt(log(1))
. In a sense, you’re sure this new function will always work.
if_
conditionsif_none
, if_any
and if_all
test the elements of the list.
if_all(1:10, ~ .x < 11, ~ return(letters[1:10]))
#> [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j"
if_any(1:10, is.numeric, ~ print("Yay!"))
#> [1] "Yay!"
if_none(1:10, is.character, ~ rnorm(10))
#> [1] -0.85634238 -0.68504455 0.08648676 0.03568832 -0.70580251
#> [6] 0.31725665 0.51636423 1.22342971 0.43175804 0.66104784
The defaut for all .p
is isTRUE
. So you can:
if_then
performs a simple “if this then do that”:
And if_else
is a wrapper around base::ifelse()
:
The stop_if
, warn_if
and message_if
are easy to use functions that send an error, a warning or a message if a condition is met. Each function has its counterpart with _not
. That returns a message if the condition is not met.
stop_if_not
is quite the same as assert_that
from the {assertthat} package, except that it can takes mappers. It is not the same as base stopifnot()
, as it doesn’t take a list of expression.
These functions are also flexible as you can pass base predicates (is.numeric, is.character…), a custom one built with mappers, or even your own testing function.
You can either choose a custom message or just let the built-in messages be printed:
x <- 12
# Stop if .x is numeric
stop_if(.x = x,
.p = is.numeric)
#> Error: Test `is.numeric` on `x` returned an error.
y <- "20"
# stop if .x is not numeric
stop_if_not(.x = y,
.p = is.numeric,
msg = "y should be numeric")
#> Error: y should be numeric
a <- "this is not numeric"
# Warn if .x is charcter
warn_if(.x = a,
.p = is.character)
#> Warning: Test `is.character` on `a` returned a warning.
b <- 20
# Warn if .x is not equal to 10
warn_if_not(.x = b,
.p = ~ .x == 10 ,
msg = "b should be 10")
#> Warning: b should be 10
c <- "a"
# Message if c is a character
message_if(.x = c,
.p = is.character,
msg = "You entered a character element")
#> You entered a character element
# Build more complex predicates
d <- 100
message_if(.x = d,
.p = ~ sqrt(.x) < 42,
msg = "The square root of your element must be more than 42")
#> The square root of your element must be more than 42
# Or, if you're kind of old school, you can still pass classic functions
e <- 30
message_if(.x = e,
.p = function(vec){
return(sqrt(vec) < 42)
},
msg = "The square root of your element must be more than 42")
#> The square root of your element must be more than 42
If you have a function with no arguments, you can pass a dot .
as first argument :
true <- function() TRUE
false <- function() FALSE
stop_if(., true, msg = "You shouldn't have internet to do that")
#> Error: You shouldn't have internet to do that
warn_if(., false,
msg = "You shouldn't have internet to do that")
message_if(., true,
msg = "Huray, you have internet \\o/")
#> Huray, you have internet \o/
If you don’t specify a .p
, the default test is isTRUE
.
That can come really handy inside a function :
stop_if
, warn_if
and message_if
all have complementary tests with _all
, _any
and _none
, which combine the if_*
and the warn_*
, stop_*
and message_*
seen before. They take a list as first argument, and a predicate. They test if any, all or none of the elements validate the predicate.
stop_if_any(iris, is.factor, msg = "Factors here. This might be due to stringsAsFactors.")
#> Error: Factors here. This might be due to stringsAsFactors.
warn_if_none(1:10, ~ .x < 0, msg = "You need to have at least one number under zero.")
#> Warning: You need to have at least one number under zero.
message_if_all(1:100, is.numeric, msg = "That makes a lot of numbers.")
#> That makes a lot of numbers.
Thanks to Romain for the name suggestion.
Questions and feedbacks welcome!
You want to contribute ? Open a PR :) If you encounter a bug or want to suggest an enhancement, please open an issue.