Introducing Catchr 0.2.0 đŁ: Learnable Condition-handling in R
07 Feb 2019 —I guess I never made a post about it, but a while ago I had my first R package accepted to CRAN: catchr 0.1.0. It started based on a function I had written that I found myself using time and time again: a function that would collect all the warnings, messages, and errors raised in the process of evaluating code, and return them with the result of the code.
I was so proud of this âcleverâ code, but something eventually happened that made me take a deep dive into the rlang package by the RStudio people. Their code was so much better than mine that I felt viscerally humiliated.
After recovering a little from the shock, I reevaluated all of my preconceived notions for the package, and outlined the tools I wanted to provide people with. From there, I basically rebuilt it from scratch in a principled way, and turned it into something Iâm actually proud of.
I present to you, catchr 0.2.0!
âExceptions?â âHandlers?â Making sense of conditions in R
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,1 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? Râs help file on the subject is basically impenetrable,2 and even though Hadley Wickham has stepped in and produced a very helpful guide on conditions, understanding whatâs going on is still pretty hard, and actually handling conditions is even harder.
At its heart, catchr is designed to make it so that you never have to read any of that stuff make dealing with conditions easy from the get-go.
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.
The olâ razzle-dazzle
Let me hit you with a few scenarios here, to help you get a sense of what catchr can do. For more details, please check out the manual and help docs, which I stuffed with information. Suppose I want to run some code, and I want to catch any warnings it produces, âhideâ them so they donât appear, and instead just store them so I can do something else with them later on. All without making my code halt or start over.
Boom, done.
But if Iâm going to need to do that a lot, and Iâm good programmer who doesnât want to retype the same code over and over again, I can make a function that can catch conditions the same way for any expression.
One line. Look at my nice, portable function:
But letâs say Iâm a naaaaasty boy3 I have a complex situation which necessitates printing something when the code raises a message and immediately exiting it, but also turning any warnings into messages, and preventing any errors from crashing everything else. *Shiver*.
I can do that too, and thatâs only beginning to scratch the surface of catchr. Although most of the detailed information can be found in the help files and manual, letâs go over some of the basics.
Catching conditions with âplansâ
Letâs take a look at some of the code we saw before:
This is pretty quintessential of the catchr system. You have the expression you want to evaluate and catch conditions for (here, arbitrary_code()
), followed by the names of the types of conditions you want to catch and the plans you have for them when theyâre caught (warning = c(collect, muffle)
).
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.4
A catchr âplanâ starts off as a named argument, where its name is the type of condition it will apply to (e.g., âmessageâ, âwarningâ, âerrorâ, etc.), and its value will eventually become a function that will take in the condition and do stuff with it. catchrâs âsecret sauceâ lets you pass in each plan as a list of functions that will be applied to the condition in order.5
But catchr also comes full of stock functions that make condition handling simpleâthese special functions can be inputted as strings, but catchr promotes more-readable code and saves you extra typing by letting you enter them as unquoted special terms, like collect
and muffle
in the example above.
Special reserved terms
These special terms cover some of the most common behaviors users might want to use:
Special âreservedâ term | functionality |
---|---|
tomessage , towarning , toerror |
convert conditions to other types of conditions |
beep |
plays 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â,6 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 collect
and muffle
were strung together to make a plan.
Even less typing
Is typing out plans multiple times still too much typing? Well, catchr lets you save even more space by giving you the option of passing in unnamed arguments into plans!
These unnamed arguments can be entered as either strings or unquoted terms, and should correspond to the name of a condition you want to catch. They will automatically be given whatever catchrâs default plan is, which can be get/set via get_default_plan()
and set_default_plan
, respectively. Named and unnamed arguments can be mixed freely. Other default behaviors can be get/set with catchr_default_opts()
.
âCollectingâ conditions
As you might have noticed, many of the previous examples use a special term, collect
. Having the ability to âcollectâ conditions is one of the most useful features in catchr.
This is particularly useful if you want to catch warnings and messages, etc. from code that takes a long time to run, where having to restart the whole process from square one would be too costly.
Or if you want to collect warnings, messages, and errors from code that is running remotely, where these conditions would not be returned with the rest of the results, such as with the future package. The following isnât a very⌠natural example, but Iâm trying to keep things simple:
OR, maybe youâre running a lot of code in parallel and want to keep track of all of the raised conditions in R (where theyâre easiest to manipulate), such as in a large-scale power simulation, or with packages such purrr.
Other goodies
catchr offers many other benefits, to the point where trying to document them all in a single vignette doesnât make sense. Iâll point out some of the more unique ones here, but the help docs are very extensive, so give them a try!
Catching âmiscâ conditions
Sometimes we donât want to have to specify every alternative condition out there, and we might just want to divide conditions into one type and âeverything elseâ.
You can do this by making plans for the âmiscâ condition, a term catchr reserves for any condition(s) that you havenât already specified. Thus:
will collect messages (without muffling them) but will exit the evaluation and return a conciliatory if any other types of conditions are raised. Note that using the all-purpose âconditionâ plan (i.e., condition = exit_with("Sorry, busted")
) would exit when a message is raised, since it isnât muffled and qualifies as having the type âconditionâ. Using âmiscâ makes sure you donât âdouble-dipâ conditions.
The âmiscâ condition is particularly useful when collecting conditionsâit lets you lump all the miscellaneous conditions collected into a single âmiscâ sublist. Since anything other than the âbig threeâ conditions (errors, messages, and warnings) are so rare, the following makes a nice little collector that lumps anything âexoticâ into the âmiscâ sublist and drops any condition sublist when that condition isnât raised:
Display conditions in style
If you have the crayon
package installed, you can display messages without raising them in whatever crayon
stylings you want (though you wonât see the styles in this post):
Play sounds when conditions are raised
Particularly useful for when youâre working in a different window and waiting for a job to finish / do something unexpected, catchr lets you play short sounds whenever conditions are raised.7 This functionality requires the beepr
package, which Iâve found absolutely worth downloading.
For example, you can have it alert you when a condition is raised via sound, display the condition without raising it, and keep going:
Or you could imagine making a wrapper that screams and stop evaluation whenever it encounters any unexpected condition:
All plans are âcalling handlersâ
This is definitely more of a technical point, but all the plans in catchr are technically evaluated in order. Currently (rlang 0.3.1
) rlang
âs condition-handling function, with_handlers
, has the relatively unintuitive property of not checking its handlers in the order they are defined, but first checking all the âexitingâ handlers, and then checking all the âcallingâ handlers. catchr actually offers a function inspired by with_handlers
that strictly checks these in order and uses rlang
âs schema (with_ordered_handlers
), but catchrâs main condition handling does one better: it gets rid of âexitingâ handlers all together.
Ok, this is beginning to get much more technical, but there are many other benefits to making all the plans (and therefore, handlers) âcallingâ handlers. For one, any plan has the option of restarting the code (via muffle
, or invoking user-created restarts).
But each plan can also act as an âexitingâ handler with the use of the exit
term, or the user_exit()
and exit_with()
functions. In essence, catchr gives you the best of both worlds!
Source Code:
All the source code for catchr is on Github, including the vignette this is based off of!
Download the released version of catchr from CRAN with:
Or the development version from GitHub with:
Please feel free to drop me a line on what you think, or if you spot any bugs!
Footnotes:
-
At least, I thought it was. Now I understand that itâs everything else that doesnât make sense. ↩
-
Accessible via
help(conditions)
. ↩ -
Ed.: you canât say that in a CRAN package ↩
-
See
help("catchr-DSL", "catchr")
for the details. ↩ -
See
help(`catchr-plans`, "catchr")
. ↩ -
i.e., âsuppressesâ, âcatchesâ, âhidesââwhatever you want to call it ↩
-
If youâre going to be doing something like this, make sure
getOption("warn")==1
 ↩