I'm using R's assertthat package and am wanting to (temporarily) output a warning instead of an error on assertion failure. What's the easiest way to do that with the assertthat package?
I realize that wanting warnings instead of errors kind of goes against what assertions are supposed to be used for. In the long term, we indeed want to be outputting errors on assertion failure. In the short term, we still want the code to function even with bad input, since the output with bad inputs is still "good enough" for now.
A simple example: suppose I have a function that takes x as input and outputs x+5. I want to output a warning if x!=3. Since we will be using assert_that ultimately, it would be nice if we can use assertthat package for the warning.
In the long term, we'll use this:
> x <- 3
> fn <- function(x) {assert_that(x==3); return(x+5)}
> fn(3)
[1] 8
> fn(4)
Error: x not equal to 3
In the short term, here's the best I have so far:
> fn <- function(x) {if(!see_if(x==3)) warning(validate_that(x==3)); return(x+5)}
> fn(3)
[1] 8
> fn(4)
[1] 9
Warning message:
In fn(4) : x not equal to 3
I'm looking for a more concise solution, if possible (best case would be passing an "output_warning" parameter to assert_that, but I don't think that exists).
I created a user defined function which accepts a string corresponding to an expression against which you would like to run validate_that() (ultimately assert_that()). The function prints a warning if the assertion fails and remains silent otherwise. See below for usage. You could easily extend this custom function to accept more than one expression if necessary. Note that I also use sys.calls() to obtain the name of the function which called this helper function. This is an important piece of information so you can correlate your warnings with the code that actually generated them.
assert_that_soft <- function(exp) {
if (!exp) {
print (paste("Error in function:",
parse(sys.calls()[[sys.nframe()-1]])) ) # name of caller
}
}
Usage:
> fn <- function(x) { assert_that_soft(x==3); return(x+5) }
> fn(3)
[1] 8
> fn(8)
[1] "Error in function: fn(8)"
[1] 13
Another option is to wrap assert_that in tryCatch.
fn <- function(x) tryCatch(assert_that(x == 3), error = function(e) warning(e), finally = return(x+5))
fn(3)
# [1] 8
fn(8)
# [1] 13
# Warning message:
# x not equal to 3
I think the easiest way to overwrite the function would be to copy most of the assert_that function as is, and call the new function by the same name so you don't need to change all the code when you go into error mode.
assert_that <- function(..., env=parent.frame()) {
res <- see_if(..., env=env)
if (res)
return(TRUE)
warning(attr(res, "msg"))
TRUE
}
fn <- function(x) { assert_that(x==3); return(x+5) }
fn(3)
# [1] 8
fn(8)
# [1] 13
# Warning message:
# In assert_that(x == 3) : x not equal to 3
I am proposing an extension of the assertthat package to allow for simple warnings, see
https://github.com/hadley/assertthat/issues/69
any feedback is welcome!
Related
I have a list of file to read. I wrote a loop, like this (it is only a example). However, some of these elements are missing and I got a message error interrupting the loop.I would like to skip the error and finish the loop, with the other elements of the list.
list<- c("ciao",
"miao",
"bau")
for (symbols in list){
a <- symbols
b <- as.Date(symbols)
c <- as.numeric(symbols)
d<- cbind(a,b,c)
write.csv(d)
}
As, far as I know, I could use the function try. I have already read some examples here, but they don't fit my need... or I don't know how to implement it.
Any idea to solve the problem?
Thank you
You can use tryCatch() for this and use the error = part to indicate what you want in the case of an error.
For example, in the below we can continue checking if the symbol == "bau" even after "miao" caused an error:
# error before it checks all args of list
list <- c("ciao", "miao", "bau")
for (symbols in list) {
if (symbols == "miao") stop("Er!")
print(symbols == "bau")
}
#> [1] FALSE
#> Error in eval(expr, envir, enclos): Er!
# now it will check all args even with error
for (symbols in list) {
tryCatch(
expr = {
if (symbols == "miao") stop("Er!")
print(symbols == "bau")
},
error = function(e) NA
)
}
#> [1] FALSE
#> [1] TRUE
Created on 2022-08-03 by the reprex package (v2.0.1)
It's complicated to explain my use case but I am working on a project that requires parsing text that may throw some errors. I would like to use tryCatch() so that as much of the script can run as possible and alert the user that some code failed. I can use a loop for this but I would like to know why this behaviour exists and if there is an apply function that does do the trick.
When I run the loop or use do.call() on this parsed object, I get just the expected single error mesage. When I use lapply() I get the same error message followed by the ouput of the assignments. I've tried throwing suppress functions around lapply() which, perhaps obviously, did not work. and I get similar output for sapply() and map(). Curious if someone can explain it to me.
test_text <- parse(text = "x <- pi; y <- x; z <- stop()")
eval_try <- function(x) {
tryCatch(
eval(x, envir = .GlobalEnv),
error = function(cond) message("there was an error"),
warning = function(cond) message("there was a warning")
)
}
for (i in seq_along(test_text)) {
eval_try(test_text[i])
}
#> there was an error
do.call("eval_try", list(test_text))
#> there was an error
lapply(test_text, eval_try)
# there was an error
# [[1]]
# [1] 3.141593
#
# [[2]]
# [1] 3.141593
#
# [[3]]
# NULL
The printing you see is the output of the lapply function. You can suppress it by assigning the result to a variable or if you really don't care about storing the output, use the below trick with invisible.
> myfun <- function(x) x
>
> lapply(1:3, FUN = myfun)
[[1]]
[1] 1
[[2]]
[1] 2
[[3]]
[1] 3
> a <- lapply(1:3, FUN = myfun)
> invisible(lapply(1:3, FUN = myfun))
I am wondering what is the difference between hasArg() and exist() in a R function. It seems that hasArg() works while exist does not work to test if an argument exists in the input of a R function.
f_exists <- function(x){
if(exists("x")){
print("exist")
}else{
print("Non existence")
}}
When I test this f_exists function, the exist seems not to be working in the ifelse statement:
> f_exists(x = 1)
[1] "exist"
> f_exists(x)
[1] "exist"
> f_exists()
[1] "exist"
However, if I use the function hasArg() in the ifelse statement, the function works:
f_hasArg <- function(x){
if(hasArg("x")){
print("exist")
}else{
print("Non existence")
}
}
> f_hasArg(x = 1)
[1] "exist"
> f_hasArg(x)
[1] "exist"
> f_hasArg()
[1] "Non existence"
However, it is weird that exist() and hasArg() seem to be working in a reversed way when I test them in the environment:
> rm(list = ls())
> exists("x")
[1] FALSE
> hasArg("x")
[1] FALSE
> x <- 1
> exists("x")
[1] TRUE
> hasArg("x")
[1] FALSE
I am asking why is hasArg() and exists() work in a different way in R functions and in the environment? Is there a underlying reason for that? Thanks.
f_exists() returns "exist", because variable x actually exists inside function f_exists, yet it is bound to a special "missing" value. Using that value would result in an error ("argument 'x' is missing, with no default), but as the value is not used, no error is reported.
hasArg("x") returns false, because it is looking really at (formal) arguments of the surrounding function call, not at any variables in the current environment. The documentation has more details: ?exists and ?hasArg.
To check whether a value has been explicitly provided by the caller for a formal argument x of a function, one can use missing(x). If the same could be achieved by providing a default argument expression, it is probably cleaner to do so.
Here is my code:
test <- function(y){
irisname <- c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width","Species")
if(y %in% irisname){
print(y)
} else{
test <- function(...) stop("dummy error")
test(y)
}
}
> test("ds")
Error in test(y) : dummy error
In the result: "Error in test(y) : dummy error ", I need "ds" in test("ds"), not test(y).
How can I do that?
This almost does it (there's an extra colon ...), by using call.=FALSE to suppress the information about the call and hacking it into the error message.
update: added quotation marks to error #1; explained a bit more about why this problem is hard.
I don't know the structure of your code, but you are making life considerably harder for yourself by passing objects farther down into the structure. It would be a lot easier to call stop() directly from within your first level, or to use the information carried in y directly within your error message.
test <- function(y,stop=FALSE){
irisname <- c("Sepal.Length","Sepal.Width",
"Petal.Length","Petal.Width","Species")
if (stop) stop(sprintf("premature stop: var %s",y))
if(y %in% irisname){
print(y)
} else{
test <- function(...) {
stop(sprintf("in test(\"%s\"): dummy error",...),
call.=FALSE)
}
test(y)
}
}
test("junk")
## Error: in test("junk"): dummy error
test("junk",stop=TRUE)
## Error in test("junk", stop = TRUE) : premature stop: var junk
Getting rid of the spurious first colon in the output of test("junk") will be considerably harder, because the Error: string is hard-coded within R. Your best bet is probably, somehow, to print your own custom error message and then stop silently, or recreate the behaviour of stop() without generating the message (see ?condition: e.g. return(invisible(simpleError("foo")))). However, you're going to have to jump through a lot of hoops to do this, and it will be hard to ensure that you get exactly the same behaviour that you would have with stop() (e.g. will the error message have been saved in the error-message buffer?)
What you want to do is probably possible by mucking around with R internals enough, but in my opinion so hard that it would be better to rethink the problem ...
Good luck.
You could check the argument right at the start of the function. match.arg might come in handy, or you could print custom message and return NA.
two updates below
> test <- function(y)
{
if(!(y %in% names(iris))){
message(sprintf('test("%s") is an error. "%s" not found in string', y, y))
return(NA) ## stop all executions and exit the function
}
return(y) ## ... continue
}
> test("Sepal.Length")
# [1] "Sepal.Length"
> test("ds")
# test("ds") is an error. "ds" not found in string
# [1] NA
Add/Edit : Is there a reason why you're nesting a function when the function goes to else? I removed it, and now get the following. It seems all you are doing is checking an argument, and end-users (and RAM) want to know immediately if they enter an incorrect default arguments. Otherwise, you're calling up unnecessary jobs and using memory when you don't need to.
test <- function(y){
irisname <- c("Sepal.Length","Sepal.Width","Petal.Length","Petal.Width","Species")
if(y %in% irisname){
print(y)
} else{
stop("dummy error")
}
}
> test("ds")
# Error in test("ds") : dummy error
> test("Sepal.Length")
# [1] "Sepal.Length"
You could also use pmatch, rather than match.arg, since match.arg prints a default error.
> test2 <- function(x)
{
y <- pmatch(x, names(iris))
if(is.na(y)) stop('dummy error')
names(iris)[y]
}
> test2("ds")
# Error in test2("ds") : dummy error
> test2("Sepal.Length")
# [1] "Sepal.Length"
Code written using lapply and friends is usually easier on the eyes and more Rish than loops. I love lapply just as much as the next guy, but how do I debug it when things go wrong? For example:
> ## a list composed of numeric elements
> x <- as.list(-2:2)
> ## turn one of the elements into characters
> x[[2]] <- "what?!?"
>
> ## using sapply
> sapply(x, function(x) 1/x)
Error in 1/x : non-numeric argument to binary operator
Had I used a for loop:
> y <- rep(NA, length(x))
> for (i in 1:length(x)) {
+ y[i] <- 1/x[[i]]
+ }
Error in 1/x[[i]] : non-numeric argument to binary operator
But I would know where the error happened:
> i
[1] 2
What should I do when using lapply/sapply?
Use the standard R debugging techniques to stop exactly when the error occurs:
options(error = browser)
or
options(error = recover)
When done, revert to standard behaviour:
options(error = NULL)
If you wrap your inner function with a try() statement, you get more information:
> sapply(x, function(x) try(1/x))
Error in 1/x : non-numeric argument to binary operator
[1] "-0.5"
[2] "Error in 1/x : non-numeric argument to binary operator\n"
[3] "Inf"
[4] "1"
[5] "0.5"
In this case, you can see which index fails.
Use the plyr package, with .inform = TRUE:
library(plyr)
laply(x, function(x) 1/x, .inform = TRUE)
Like geoffjentry said:
> sapply(x, function(x) {
res <- tryCatch(1 / x,
error=function(e) {
cat("Failed on x = ", x, "\n", sep="") ## browser()
stop(e)
})
})
Also, your for loop could be rewritten to be much cleaner (possibly a little slower):
> y <- NULL
> for (xi in x)
y <- c(y, 1 / xi)
Error in 1/xi : non-numeric argument to binary operator
For loops are slow in R, but unless you really need the speed I'd go with a simple iterative approach over a confusing list comprehension.
If I need to figure out some code on the fly, I'll always go:
sapply(x, function(x) {
browser()
...
})
And write the code from inside the function so I see what I'm getting.
-- Dan
Using debug or browser isn't a good idea in this case, because it will stop your code so frequently. Use Try or TryCatch instead, and deal with the situation when it arises.
You can debug() the function, or put a browser() inside the body. This is only particularly useful if you don't have a gajillion iterations to work through.
Also, I've not personally done this, but I suspect you could put a browser() in as part of a tryCatch(), such that when the error is generated you can use the browser() interface.
I've faced the same problem and have tended to make my calls with (l)(m)(s)(t)apply to be functions that I can debug().
So, instead of blah<-sapply(x,function(x){ x+1 })
I'd say,
myfn<-function(x){x+1}
blah<-sapply(x,function(x){myfn(x)})
and use debug(myfn) with options(error=recover).
I also like the advice about sticking print() lines here and there to see what is happening.
Even better is to design a test of myfn(x) that it has to pass and to be sure it passes said test before subjecting it to sapply. I only have patience to to this about half the time.