Git Product home page Git Product logo

catchr's Introduction

Travis-CI Build Status CRAN status

Catchr: the friendlier way of catching errors, warnings, and conditions

“Exceptions?” “Handlers?” Making sense of conditions

Compared to many other programming languages, the way R handles ‘conditions’—errors, warnings, messages, and most things referred to as ‘exceptions’ in other languages—is pretty unintuitive, and can often be troublesome to users coming from different backgrounds. For example, on the surface the way exceptions are caught in Python seems so simple compared to R—what even is a “restart”? What are those things people are referring to as “handlers” anyway?

The purpose of catchr is to provide flexible, useful tools for handling R conditions with less hassle and head-scratching. One of the most important goals of this package is to maintain a continuous learning curve that so new users jump straight in, but more advanced users can find the depth and complexity they need to take advantage of R’s powerful condition-handling abilities.

To lower the barrier of entry, keep code clean and readable, and reduce the amount of typing required, catchr uses a very simple domain-specific language that simplifies things on the front-end. catchr focuses on letting users build their own “catching” functions where they can specify behavior via conceptual “plans”, removing unnecessary complexities—like the distinction between “calling” vs. “exiting” handlers—and adding many useful features, like the ability to “collect” the conditions raised from a call.

Installation

You can install the released version of catchr from CRAN with:

install.packages("catchr")

And the development version from GitHub with:

# install.packages("devtools")
devtools::install_github("burchill/catchr")

Introduction

In R, “warnings” (which generally indicate something might be going wrong), “errors” (which indicate something definitely has gone wrong), and “messages” (which generally just indicate neutral information) are all subclasses of “conditions”, and these three types make up a vast majoirty of the conditions you will ever encounter if you’re not a developer.

When a condition is “raised”, the code essentially stops, and the condition floats up through the code until something “catches” it or not. If nothing catches it and deals with it, warnings, messages, and errors will print a message out on your screen. Then, unless the condition was an error, the code picks up where it left off.

Phrased as such, conditions may seem like no big deal. And for many basic uses of R, maybe they’re not; if you’re just tidying up some data and making a plot out of it, you can react to warnings and errors as they come, with little cost. But for more involved R projects, being able to deal with conditions programmatically becomes indispensable.

A basic example

A (somewhat sassy) introduction to catchr can be found in the vignettes (vignette("welcome-to-catchr","catchr") if you’ve installed it). Here, we’ll just cover some cases to demonstrate what the code looks like, and some of the advantages it offers.

Let’s look at a very simple case first. As you may know, trying to take the log of a negative number raises a warning and returns a NaN. There are times where it would be important not to encounter a NaN, and maybe you want the code to stop whenever a warning of any kind is raised.

library(catchr)

fake_model <- function(x, err = F) {
  y <- log(x)
  if (err) stop("Uh oh!")
  c(y, x+1) 
}
# Works fine
fine_results <- catch_expr(fake_model(5), warning = toerror)

But when a NaN is made and a warning is raised, catchr converts the warning into an error and the code stops:

# Stops the code
bad_results <- catch_expr(fake_model(-7), warning = toerror)

But let’s say you want to be alerted about this issue as soon as possible, and you’re working on something else in a different window while the code runs. You can have catchr play a beeping sound whenever this event happens with a simple addition:

# Stops the code and make a beeping sound
bad_results <- catch_expr(fake_model(-7), warning = c(beep, toerror))

catchr is designed so that making “plans” for a condition is simple, extendable, and flexible. In the example above, we made a “plan” for conditions of the class “warning” so that when one is raised, first a beep is played and then the warning is converted to an error.

catchr “plans”

Instead of using R’s “calling”/“exiting” “handler” terminology, catchr keeps things simple with a single concept, “plans”. In catchr, users use functions like building blocks to a “plan” of what to do for particular conditions. Users can specify their own functions or use catchr functions, but catcher also offers a useful toolbox of behaviors that work their magic behind the scene through catchr’s simple domain-specific language.[1]

This toolbox consists of special “reserved” terms that users can input as strings or unquoted terms, and cover some of the most common behaviors users might want to use:

Special “reserved” term Function
tomessage, towarning, toerror convert conditions to other types of conditions
beep play a short sound
display displays the contents of the condition on-screen
collect collects the condition and saves it to a list that will be returned at the end
muffle “muffles”,[2] a condition so it doesn’t keep going up, and restarts the code
exit immediately stops the code and muffles the condition
raise raises conditions past exit

These can be used as building blocks just like normal functions. For example, in the previous example, we saw how beep and toerror were strung together to make a plan.

Reusability

catchr is all about keeping code minimal, and is built around reusability. You can make and reuse plans across expressions:

# Since all conditions have class "condition", this is a plan for all conditions
plans <- make_plans(condition = c(collect, muffle))
plans
#> <catchr_compiled_plans>
#> condition: c(collect, muffle)
#>   to see catchr options, use `summary()`
res1 <- catch_expr(fake_model(-4.0, err = F), plans)
res1
#> $value
#> [1] NaN  -3
#> 
#> $condition
#> $condition[[1]]
#> <simpleWarning in log(x): NaNs produced>
res2 <- catch_expr(fake_model(-3.9, err = T), plans)
res2
#> $value
#> NULL
#> 
#> $condition
#> $condition[[1]]
#> <simpleWarning in log(x): NaNs produced>
#> 
#> $condition[[2]]
#> <simpleError in fake_model(-3.9, err = T): Uh oh!>

And even more importantly, you can create your own functions that you can use to catch conditions for any code:

collect_and_muffle <- make_catch_fn(plans)

res1 <- collect_and_muffle(fake_model(-4.0, err = F))
res2 <- collect_and_muffle(fake_model(-3.9, err = T))

“Collecting” conditions

One of the most useful things about catchr is its ability to catch and store any conditions raised during evaluation with the collect term, returning the conditions after the code is finished without restarting the evaluation from scratch.

There are a number of situations in which this can be immensely handy. For example, if you’re trying to catch warning messages from code that takes a long time to run, where having to restart the whole process from the beginning would be too costly.

With future

catchr can be incredibly useful when trying to diagnose code run in parallel or on remote machines, like it is with future. Although future has come a long way in terms of how easy it is to debug (because Henrik Bengtsson is both a saint and a genius), but capturing and returning every condition that was raised is easy with catchr.

library(future)
future::plan(multiprocess) # you could use `remote` or whatever you need
#> Warning: [ONE-TIME WARNING] Forked processing ('multicore') is disabled
#> in future (>= 1.13.0) when running R from RStudio, because it is
#> considered unstable. Because of this, plan("multicore") will fall
#> back to plan("sequential"), and plan("multiprocess") will fall back to
#> plan("multisession") - not plan("multicore") as in the past. For more details,
#> how to control forked processing or not, and how to silence this warning in
#> future R sessions, see ?future::supportsMulticore

future_res %<-% collect_and_muffle(fake_model(-99, err = TRUE))
future_res
#> $value
#> NULL
#> 
#> $condition
#> $condition[[1]]
#> <simpleWarning in log(x): NaNs produced>
#> 
#> $condition[[2]]
#> <simpleError in fake_model(-99, err = TRUE): Uh oh!>
# If you wanted to raise all the conditions on your local machine and return the value of the evaluated expression:
result <- dispense_collected(future_res)

With purrr

Collecting conditions is also great with purrr or scenarios where you want to apply functions programmatically—for example, if you’re running a bunch of models via map.[3] If you want to capture which models had which problems (and then print them all pretty), it’s trivial to do so.

library(purrr)
# Let's assume `l` came from running a bunch of models,
#   e.g., `map(datasets, ~collect_and_muffle(model_func(.)))`
results <- l %>% imap(function(e, i) {
  cat("\n")
  cat("in l[[",i,"]]:\n", sep = "")
  dispense_collected(e, treat_errs="display") })
#> 
#> in l[[1]]:
#> Warning: Bad eigenvalues, bro
#> Warning: Convergence failure!
#> 
#> in l[[2]]:
#> Dropping contrasts
#> Warning: Were those contrasts important?
#> 
#> in l[[3]]:
#> I'm tired of this data!
print(results)
#> [[1]]
#> [1] "model-1"
#> 
#> [[2]]
#> [1] "model-2"
#> 
#> [[3]]
#> NULL

Found a bug or have a suggestion?

Please open an issue and I’ll try to get to it!

Footnotes

  1. See help("catchr-DSL", "catchr") for the details.

  2. i.e., “suppresses”, “catches”, “hides”—whatever you want to call it

  3. I’ve found it’s even more useful when you combine purrr and future via furrr (e.g., to run models in parallel). Shout-out to Davis Vaughan for his lovely code!

catchr's People

Contributors

burchill avatar lionel- avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

lionel-

catchr's Issues

catchr and rlang 1.0.0

Hello,

I'm seeing several errors with dev rlang. We plan to release it in 2 to 4 weeks. With R CMD check I see these two:

check_and_clean_input(d1 = base::acosh, spec_names = "acosh")
#> Error in `call_name()`: `call` must be a simple call.
#> ℹ Calls to `::` or `:::` are not simple calls.
#> ℹ See `?is_call_simple`.


opts <- catchr_opts(
  default_plan = c(collect, muffle),
  drop_empty_conds = FALSE,
  bare_if_possible = FALSE
)
plans <- make_plans(warning, message, error, .opts = opts)

res <- catch_expr(condition_thrower(), plans)
res2 <- catch_expr(dispense_collected(res), plans)
waldo::compare(res, res2)
#> `old$error[[1]]` is length 2
#> `new$error[[1]]` is length 3
#>
#> `names(old$error[[1]])`: "message" "call"
#> `names(new$error[[1]])`: "message" "call" "trace"
#>
#> `old$error[[1]]$trace` is absent
#> `new$error[[1]]$trace` is an S3 object of class <rlang_trace/rlib_trace/tbl/data.frame>, a list

The call_name() error is a planned breakage, it now needs to be paired with is_call_simple().

I see more issues with interactive devtools::test() but haven't investigated. Could you take a look and let me know if you think there is any bug in rlang that causes failures please?

New version of `rlang` causes error with dots (...)

I recently updated rlang to v 0.4.11.9000 and am now encountering an error with make_plans and make_catch_fn that has to do with the function catchr:::approx_arg_name and how expr_deparse now deals with ....

Error trace:

`...` is not empty.
i These dots only exist to allow future extensions and should be empty.
x We detected these problematic arguments:
* `..1`
i Did you misspecify an argument?
Backtrace:
     x
  1. +-base::source("~/R/Quant/JobsScripts/TuneHP.R", echo = TRUE)
  2. | +-base::withVisible(eval(ei, envir))
  3. | \-base::eval(ei, envir)
  4. |   \-base::eval(ei, envir)
  5. +-qf::start_cluster(outfile = "~/R/Quant/cl_log.log") ~/R/Quant/JobsScripts/TuneHP.R:12:0
  6. | +-base::eval(...)
  7. | | \-base::eval(...)
  8. | \-catchr::make_catch_fn(...)
  9. |   \-catchr::make_plans(..., .opts = .opts)
 10. |     \-catchr:::check_and_clean_input(..., spec_names = special_terms)
 11. |       \-catchr:::clean_input(akw$kwargs, spec_names)
 12. |         \-`%>%`(...)
 13. +-catchr:::add_back_arg_pos(., qs)
 14. | \-purrr::map2(...)
 15. +-purrr::map(., ~classify_arg(., spec_names))
 16. | \-catchr:::.f(.x[[i]], ...)
 17. |   \-catchr:::classify_arg(., spec_names)
 18. |     \-catchr:::approx_arg_name(!!arg)
 19. |       \-get_expr(enquo(x)) %>% expr_deparse(999) %>% paste(collapse = "")
 20. +-base::paste(., collapse = "")
 21. \-rlang::expr_deparse(., 999)
 22.   \-rlang::check_dots_empty0(...)
 23.     \-rlang::check_dots_empty()
 24.       \-rlang:::action_dots(...)

SessionInfo:

R version 4.0.2 (2020-06-22)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 19042)

Matrix products: default

locale:
[1] LC_COLLATE=English_United States.1252 
[2] LC_CTYPE=English_United States.1252   
[3] LC_MONETARY=English_United States.1252
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.1252    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods  
[7] base     

other attached packages:
[1] parsnip_0.1.5        RevoUtils_11.0.2     RevoUtilsMath_11.0.0

loaded via a namespace (and not attached):
 [1] colorspace_2.0-0    slider_0.1.5        ellipsis_0.3.1     
 [4] class_7.3-17        timetk_2.6.0        fs_1.5.0           
 [7] logspline_2.1.16    rstudioapi_0.13     mice_3.13.0        
[10] listenv_0.8.0       furrr_0.2.1         dials_0.0.9.9000   
[13] bit64_4.0.5         prodlim_2019.11.13  fansi_0.4.2        
[16] mvtnorm_1.1-1       lubridate_1.7.9.2   codetools_0.2-16   
[19] splines_4.0.2       knitr_1.31          jsonlite_1.7.2     
[22] workflows_0.2.2     pROC_1.16.2         broom_0.7.6        
[25] anytime_0.3.9       yardstick_0.0.8     tune_0.1.5         
[28] compiler_4.0.2      httr_1.4.2          tictoc_1.0         
[31] backports_1.2.1     assertthat_0.2.1    Matrix_1.2-18      
[34] cli_2.5.0           htmltools_0.5.1.1   tools_4.0.2        
[37] gtable_0.3.0        glue_1.4.2          dplyr_1.0.5        
[40] Rcpp_1.0.6          DiceDesign_1.8-1    vctrs_0.3.8.9000   
[43] debugme_1.1.0       iterators_1.0.13    timeDate_3043.102  
[46] gower_0.2.2         xfun_0.22           stringr_1.4.0      
[49] globals_0.14.0      lifecycle_1.0.0     qf_0.0.0.9999      
[52] future_1.20.1       tsibble_0.9.3       MASS_7.3-51.6      
[55] zoo_1.8-8           scales_1.1.1        ipred_0.9-9        
[58] parallel_4.0.2      expm_0.999-4        yaml_2.2.1         
[61] quantmod_0.4.17     curl_4.3            Exact_2.0          
[64] memoise_1.1.0       ggplot2_3.3.3       rpart_4.1-15       
[67] stringi_1.5.3       RSQLite_2.2.2       foreach_1.5.1      
[70] TTR_0.23-6          lhs_1.1.1           warp_0.2.0         
[73] boot_1.3-25         lava_1.6.8.1        rlang_0.4.11.9000  
[76] pkgconfig_2.0.3     rsample_0.0.9       evaluate_0.14      
[79] lattice_0.20-41     purrr_0.3.4         recipes_0.1.16     
[82] bit_4.0.4           tidyselect_1.1.0    parallelly_1.21.0  
[85] plyr_1.8.6          magrittr_2.0.1      R6_2.5.0           
[88] DescTools_0.99.37   generics_0.1.0      DBI_1.1.1          
[91] pillar_1.5.1        withr_2.3.0         xts_0.12.1         
[94] RPushbullet_0.3.3.1 survival_3.1-12     nnet_7.3-14        
[97] tibble_3.1.0        crayon_1.4.1        catchr_0.2.3       
[100] utf8_1.2.1          AlpacaforR_1.0.0    rmarkdown_2.7.1    
[103] workflowsets_0.0.2  grid_4.0.2          data.table_1.13.2  
[106] blob_1.2.1          digest_0.6.27       tidyr_1.1.3        
[109] GPfit_1.0-8         munsell_0.5.0 

!! Bang-Bang operator causes error: Error in enquo(expr) : object 'x' not found

Hi @burchill,
I've been thoroughly making use of catchr thanks to the info you provided in the issue over on furrr, thank you for cluing me into this useful package and for your dedicated effort in creating and maintaining it!

I've just encountered a somewhat bizarre error when using catchr to surface message from background processes using the implementation you provided. The bang bang !! operator just doesn't seem to work when catch is used!

Here's a reprex to illustrate:

write_to_file <- function(cond) {
  cond_class <- class(cond)[1]
  msg <- paste(cond_class, ":", cond$message)
  write(msg, file = "outlog", append=TRUE)
}

catch <- catchr::make_catch_fn(
  warning = c(write_to_file, muffle),
  message = c(write_to_file, muffle),
  error   = c(write_to_file, catchr::exit_with("Returned error!"))
)
cl <- parallel::makeCluster(2)
future::plan(future::cluster, workers = cl)

.d <- data.frame(time = rnorm(5), t2 = rnorm(5))
`!!` <- rlang::`!!`
`%>%` <- magrittr::`%>%`
# With normal furrr it's fine
furrr::future_imap(1:2, ~{
  .t <- "time"
  .ts <- rlang::sym(.t)
  .d %>% 
    dplyr::select(!!.ts)
})
# But with catchr the bang bang causes an error
furrr::future_imap(1:2, ~catch({
  .t <- "time"
  .ts <- rlang::sym(.t)
  .d %>% 
    dplyr::select(!!.ts)
}))

parallel::stopCluster(cl)

Error in enquo(expr) : object '.ts' not found

I should also mention that apparently there's a new immediateMessage class in future functionality that allows messages to be surfaced to the console from background processes. You might already know about it, but if you haven't I just recently tagged you again on the issue where you informed me about catchr where Davis Vaughan and Henrik Bengtsson shared it with me. I feel like it's info that is likely useful you and this package.

Here's my sessionInfo JIC you need it:

R version 3.5.3 (2019-03-11)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 18362)

Matrix products: default

locale:
[1] LC_COLLATE=English_United States.1252  LC_CTYPE=English_United States.1252   
[3] LC_MONETARY=English_United States.1252 LC_NUMERIC=C                          
[5] LC_TIME=English_United States.1252    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] RevoUtils_11.0.3     AlpacaforR_1.0.0     printr_0.1           dplyr_1.0.0          magrittr_1.5        
[6] RevoUtilsMath_11.0.0

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.5.2         HDA_0.0.0.9000       catchr_0.2.2         pillar_1.4.6.9000    compiler_3.5.3      
 [6] tools_3.5.3          digest_0.6.25.1      packrat_0.5.0        bit_1.1-15.2         lubridate_1.7.9     
[11] jsonlite_1.7.0       RSQLite_2.2.0.9000   memoise_1.1.0        lifecycle_0.2.0.9000 tibble_3.0.3.9000   
[16] pkgconfig_2.0.3      rlang_0.4.7.9000     DBI_1.1.0            rstudioapi_0.11      parallel_3.5.3      
[21] xfun_0.15.1          furrr_0.1.0.9002     httr_1.4.1           stringr_1.4.0        knitr_1.29.3        
[26] globals_0.12.5       generics_0.0.2       vctrs_0.3.2          bit64_0.9-7          websocket_1.1.0     
[31] tidyselect_1.1.0     glue_1.4.1.9000      listenv_0.8.0        R6_2.4.1             tidyr_1.1.0         
[36] purrr_0.3.4.9000     blob_1.2.1           codetools_0.2-16     ellipsis_0.3.1       future_1.18.0       
[41] stringi_1.4.7        crayon_1.3.4   

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.