3

I want to make informative stopifnot() errors.

I've read: http://r-pkgs.had.co.nz/tests.html (the section at the end on using NSE to make informative test error print out for the example seems relevant) and http://adv-r.had.co.nz/Computing-on-the-language.html but I cant get this to print an informative error in concise code:

e <- new.env()
e$label <- c(1,2,3)
check_in_y <- function(x, z, e) {
  stopifnot(eval(bquote(.(x) %in% e[[.(z)]])))
}

check_in_y(5,"label", e)

The output gives this (not so informative)

Error: eval(bquote(.(x) %in% e[[.(z)]])) is not TRUE

I want the error to be more informative, saying this:

Error: 5 %in% e[["label"]] is not TRUE

How can I get this to work? Or what's the best approach to achieve what I want

I know I could write an if condition not true then print my own error as an alternative, but the extra code is a hassle. I'd like to understand how to get NSE to get this to work.

Edit: My motivation from this approach came from reading hadley's comments (at http://r-pkgs.had.co.nz/tests.html):

However, if the expectation fails this doesn’t give very informative output:

expect_floor_equal("year", "2008-01-01 00:00:00")
## Error: floor_date(base, unit) not equal to as.POSIXct(time, tz = "UTC")
## Mean absolute difference: 31622400

Instead you can use a little non-standard evaluation to produce something more informative. The key is to use bquote() and eval(). In the bquote() call below, note the use of .(x) - the contents of () will be inserted into the call.

expect_floor_equal <- function(unit, time) {
  as_time <- function(x) as.POSIXct(x, tz = "UTC")
  eval(bquote(expect_equal(floor_date(base, .(unit)), as_time(.(time)))))
}
expect_floor_equal("year", "2008-01-01 00:00:00")
## Error: floor_date(base, "year") not equal to as_time("2008-01-01 00:00:00")
  • Side note: you should probably have `.(e)` inside the function, right? – Frank Apr 13 '17 at 15:01
  • Either way.... don't want to complicate the example too much for now –  Apr 13 '17 at 15:04
  • 1
    I'm not sure I get your point but I'm quite happy with `checkmate` which extends Hadley's `testthat`package. However, it is one of the many packages on CRAN tackling this issue. `checkmate::assert_choice(5, e[["label"]])` returns the message: `Error in check_in_y(5, "label", e) : Assertion on 'x' failed: Must be element of set {'1','2','3'}.` It also works in your function as replacement for `stopifnot()`. – Uwe Apr 13 '17 at 16:11
  • Thanks, didn't know about this package. Yes, you're addressing my point. If you had this as an answer i'd +1 –  Apr 13 '17 at 16:18
  • Great, just posted the answer – Uwe Apr 13 '17 at 16:33

2 Answers2

6

stopifnot is just a convenience function for

if(!all(condition)) stop(standard message)

For custom messages, just write the code. You can replace the stopifnot call with two lines:

check_in_y <- function(x, z, e) {
    b <- bquote(.(x) %in% e[[.(z)]])
    if(!eval(b)) stop(deparse(b), " is not TRUE", call. = FALSE)
}

check_in_y(5, "label", e)
# Error: 5 %in% e[["label"]] is not TRUE
BrodieG
  • 51,669
  • 9
  • 93
  • 146
Rich Scriven
  • 97,041
  • 11
  • 181
  • 245
  • Thanks. I wish there was a quicker way to write informative `stopifnot` errors for various types of checks (one reason for using `stopifnot` was to avoid writing `if (test) stop("state error")` sort of things to save amount of code needed to write various types of error checks like this one... but general).... Maybe my approach with NSE isn't the right way to get informative errors with very little code. –  Apr 13 '17 at 15:15
  • I understand. Stepping back a bit, originally I had `check_in_y <- function(x, z, e) { stopifnot(x %in% e[[z]]) }` for which `check_in_y(5,"label", e)` printed `Error: x %in% e[[z]] is not TRUE`. I was hoping to make that print out more informative (z could be a range of variables in the environment, so i figured it would be nice to print to the user which variable in the environment exactly). –  Apr 13 '17 at 15:28
4

There are a number of packages on CRAN which address the issue of meaningful error messages. I have started with the assertthat and assertive packages but I'm now using checkmate for production code, especially for checking arguments to functions. BTW, checkmate also extends Hadley's testthat package.

With checkmate,

checkmate::assert_choice(5, e[["label"]])

as well as

checkmate::assert_choice(5, e$label)

return the error message:

Error: Assertion on '5' failed: Must be element of set {'1','2','3'}, but is '5'.

It can also be used in the function

check_in_y <- function(x, z, e) {
  checkmate::assert_choice(x, e[[z]])
}
check_in_y(5, "label", e)

which returns the error message:

Error in check_in_y(5, "label", e) :
Assertion on 'x' failed: Must be element of set {'1','2','3'}, but is '5'.

Uwe
  • 41,420
  • 11
  • 90
  • 134