bookclub-advr

DSLC Advanced R Book Club
git clone https://git.eamoncaddigan.net/bookclub-advr.git
Log | Files | Refs | README | LICENSE

commit e28ddb56a9a449b5c1a54515c86db259c96b6ba2
parent 5c6bf7e6a270e41d1a99fdb0071f87a1ac2583d7
Author: Arthur Shaw <47256431+arthur-shaw@users.noreply.github.com>
Date:   Thu, 18 Aug 2022 15:28:53 -0400

Chapter 8 - Conditions (#18)

* Add `{emoji}` for communicative emojis

* Add commonly used libraries

* Capture/display errors in code chunks

* Draft notes

* Fix typo.

Co-authored-by: Jon Harmon <jonthegeek@gmail.com>
Diffstat:
M08_Conditions.Rmd | 491++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MDESCRIPTION | 6+++++-
Mindex.Rmd | 3++-
3 files changed, 494 insertions(+), 6 deletions(-)

diff --git a/08_Conditions.Rmd b/08_Conditions.Rmd @@ -2,12 +2,495 @@ **Learning objectives:** -- THESE ARE NICE TO HAVE BUT NOT ABSOLUTELY NECESSARY +- What conditions are +- How to use them -## SLIDE 1 +## Introduction -- ADD SLIDES AS SECTIONS (`##`). -- TRY TO KEEP THEM RELATIVELY SLIDE-LIKE; THESE ARE NOTES, NOT THE BOOK ITSELF. +What are conditions? Problems that happen in functions: + +- Error +- Warning +- Message + +As a function author, one can signal them--that is, say there's a problem. + +As a function consumer, one can handle them--for example, react or ignore. + +## Signalling conditions + +### Types of conditions + +Three types of conditions: + +- `r emoji::emoji("x")` **Errors.** Problem arose, and the function cannot continue. +- `r emoji::emoji("warning")` **Warnings.** Problem arose, but the function can continue, if only partially. +- `r emoji::emoji("speech_balloon")` **Messages.** Something happened, and the user should know. + +### `r emoji::emoji("x")` Errors + +How to throw errors + +```{r throwing_errors} +# with base R +stop("... in the name of love...") + +# with rlang +rlang::abort("...before you break my heart...") + +# with base R; without call +stop("... think it o-o-over...", call. = FALSE) +``` +Composing error messages + +- Mechanics. + - `stop()` pastes together arguments +```{r} +some_val <- 1 +stop("Your value is: ", some_val, call. = FALSE) +``` + - `abort()` requires `{glue}` +```{r} +some_val <- 1 +rlang::abort(glue::glue("Your value is: {some_val}")) +``` +- Style. See [here](http://style.tidyverse.org/error-messages.html). + +### `r emoji::emoji("warning")` Warnings + +May have multiple warnings per call + +```{r} +warn <- function() { + warning("This is your first warning") + warning("This is your second warning") + warning("This is your LAST warning") +} +``` + +Print all warnings once call is complete. + +```{r} +warn() +``` + +Like errors, `warning()` has + +- a call argument +- an `{rlang}` analog + +```{r} +# base R +# ... with call (implicitly .call = TRUE) +warning("Warning") +# ... with call suppressed +warning("Warning", call. = FALSE) + +# rlang +# note: call suppressed by default +rlang::warn("Warning") +``` + +(Hadley's) advice on usage: + +- Err on the side of errors. In other words, error rather than warn. +- But warnings make sense in a few cases: + - Function is being deprecated. Warn that it is reaching end of life. + - Function is reasonably sure to recover from issue. + +### `r emoji::emoji("speech_balloon")` Messages + +Mechanics: + +- Issued immediately +- Do not have a call argument + +Style: + +Messages are best when they inform about: + +- Default arguments +- Status updates of for functions used primarily for side-effects (e.g., interaction with web API, file downloaded, etc.) +- Progress of long-running process (in the absence of a status bar). +- Package loading message (e.g., attaching package, objects masked) + +## Ignoring conditions + +A few ways: + +- `try()` +- `suppressWarnings()` +- `suppressMessages()` + +### `try()` + +What it does: + +- Displays error +- But continues execution after error + +```{r} +bad_log <- function(x) { + try(log(x)) + 10 +} + +bad_log("bad") +``` + +Better ways to react to/recover from errors: + +1. Use `tryCatch()` to "catch" the error and perform a different action in the event of an error. +1. Set a default value inside the call. See below. + +```{r} +default <- NULL +try(default <- read.csv("possibly-bad-input.csv"), silent = TRUE) +``` + + +### `suppressWarnings()`, `suppressMessages()` + +What it does: + +- Supresses all warnings (messages) + +```{r} +# suppress warnings (from our `warn()` function above) +suppressWarnings(warn()) + +# suppress messages +many_messages <- function() { + message("Message 1") + message("Message 2") + message("Message 3") +} + +suppressMessages(many_messages()) +``` + +## Handling conditions + +Every condition has a default behavior: + +- `r emoji::emoji("x")` Errors halt execution +- `r emoji::emoji("warning")` Warnings are collected during execution and displayed in bulk after execution +- `r emoji::emoji("speech_balloon")` Messages are displayed immediately + +Condition handlers allow one to change that behavior (within the scope of a function). + +Two handler functions: + +- `tryCatch()` +- `withCallingHandlers()` + +```{r, eval=FALSE} +# try to run `code_to_try_to_run` +# if (error) condition is signalled, fun some other code +tryCatch( + error = function(cnd) { + # code to run when error is thrown + }, + code_to_try_to_run +) + +# try to `code_to_try_to_run` +# if condition is signalled, run code corresponding to condition type +withCallingHandlers( + warning = function(cnd) { + # code to run when warning is signalled + }, + message = function(cnd) { + # code to run when message is signalled + }, + code_to_try_to_run +) +``` + + +### Condition objects + +```{r} +# catch a condition +cnd <- rlang::catch_cnd(stop("An error")) +# inspect it +str(cnd) +``` + +The standard components + +- `message`. The error message. To extract it, use `conditionMessage(cnd)`. +- `call`. The function call that triggered the condition. To extract it, use `conditionCall(cnd)`. + +But custom conditions may contain other components. + +### Exiting handlers + +If a condition is signalled, this type of handler controls what code to run before exiting the function call. + +```{r} +f3 <- function(x) { + tryCatch( + # if error signalled, return NA + error = function(cnd) NA, + # try to run log + log(x) + ) +} + +f3("x") +``` + +When a condition is signalled, control moves to the handler and never returns to the original code. + +```{r} +tryCatch( + message = function(cnd) "There", + { + message("Here") + stop("This code is never run!") + } +) +``` + +The `tryCatch()` exit handler has one final argument: `finally`. This is run regardless of the condition of the original code. This is often used for clean-up. + +```{r} +# try to write text to disk +# if an error is signalled--for example, `path` does not exist +# or if no condition is signalled +# that is in both cases, the code block in `finally` is executed +path <- tempfile() +tryCatch( + { + writeLines("Hi!", path) + # ... + }, + finally = { + # always run + unlink(path) + } +) +``` + +### Calling handlers + +Definition by verbal comparison: + +- With exit handlers, code exits the normal flow once a condition is signalled +- With calling handlers, code continues in the normal flow once control is returned by the handler. + +Definition by code comparison: + +```{r} +# with an exit handler, control moves to the handler once condition signalled and does not move back +tryCatch( + message = function(cnd) cat("Caught a message!\n"), + { + message("Someone there?") + message("Why, yes!") + } +) + +# with a calling handler, control moves first to the handler and the moves back to the main code +withCallingHandlers( + message = function(cnd) cat("Caught a message!\n"), + { + message("Someone there?") + message("Why, yes!") + } +) +``` + +### By default, conditions propagate + +Let's suppose that there are nested handlers. If a condition is signalled in the child, it propagates to its parent handler(s). + +```{r} +# Bubbles all the way up to default handler which generates the message +withCallingHandlers( + message = function(cnd) cat("Level 2\n"), + withCallingHandlers( + message = function(cnd) cat("Level 1\n"), + message("Hello") + ) +) + +# Bubbles up to tryCatch +tryCatch( + message = function(cnd) cat("Level 2\n"), + withCallingHandlers( + message = function(cnd) cat("Level 1\n"), + message("Hello") + ) +) +``` + +### But conditions can be muffled + +If one wants to "muffle" the siginal, one needs to use `rlang::cnd_muffle()` + +```{r} +# Muffles the default handler which prints the messages +withCallingHandlers( + message = function(cnd) { + cat("Level 2\n") + rlang::cnd_muffle(cnd) + }, + withCallingHandlers( + message = function(cnd) cat("Level 1\n"), + message("Hello") + ) +) + +# Muffles level 2 handler and the default handler +withCallingHandlers( + message = function(cnd) cat("Level 2\n"), + withCallingHandlers( + message = function(cnd) { + cat("Level 1\n") + rlang::cnd_muffle(cnd) + }, + message("Hello") + ) +) +``` + +### Call stacks + +Call stacks of exiting and calling handlers differ. + +Why? + +> Calling handlers are called in the context of the call that signalled the condition +> exiting handlers are called in the context of the call to tryCatch() + +To see this, consider how the call stacks differ for a toy example. + +```{r} +# create a function +f <- function() g() +g <- function() h() +h <- function() message + +# call stack of calling handlers +withCallingHandlers(f(), message = function(cnd) { + lobstr::cst() + rlang::cnd_muffle(cnd) +}) + +# call stack of exit handlers +tryCatch(f(), message = function(cnd) lobstr::cst()) +tryCatch(f(), message = function(cnd) lobstr::cst()) +``` + +## Custom conditions + +### Motivation + +The `base::log()` function provides a minimal error message. + +```{r} +log(letters) +log(1:10, base = letters) +``` + +One could make a more informative error message about which argument is problematic. + +```{r} +my_log <- function(x, base = exp(1)) { + if (!is.numeric(x)) { + rlang::abort(paste0( + "`x` must be a numeric vector; not ", typeof(x), "." + )) + } + if (!is.numeric(base)) { + rlang::abort(paste0( + "`base` must be a numeric vector; not ", typeof(base), "." + )) + } + + base::log(x, base = base) +} +``` + +Consider the difference: + +```{r} +my_log(letters) +my_log(1:10, base = letters) +``` + + +### Signalling + +Create a helper function to describe errors: + +```{r} +abort_bad_argument <- function(arg, must, not = NULL) { + msg <- glue::glue("`{arg}` must {must}") + if (!is.null(not)) { + not <- typeof(not) + msg <- glue::glue("{msg}; not {not}.") + } + + rlang::abort( + "error_bad_argument", # <- this is the (error) class, I believe + message = msg, + arg = arg, + must = must, + not = not + ) +} +``` + +Rewrite the log function to use this helper function: + +```{r} +my_log <- function(x, base = exp(1)) { + if (!is.numeric(x)) { + abort_bad_argument("x", must = "be numeric", not = x) + } + if (!is.numeric(base)) { + abort_bad_argument("base", must = "be numeric", not = base) + } + + base::log(x, base = base) +} +``` + +See the result for the end user: + +```{r} +my_log(letters) +my_log(1:10, base = letters) +``` + +### Handling + +Use class of condition object to allow for different handling of different types of errors + +```{r} +tryCatch( + error_bad_argument = function(cnd) "bad_argument", + error = function(cnd) "other error", + my_log("a") +) +``` + +But note that the first handler that matches any of the signal's class, potentially in a vector of signal classes, will get control. So put the most specific handlers first. + +## Applications + +See [the sub-section in the book](https://adv-r.hadley.nz/conditions.html#condition-applications) for excellent examples. + +## Resources + +- Conditions articles in rlang vignettes: + - [Including function calls in error messages](https://rlang.r-lib.org/reference/topic-error-call.html) + - [Including contextual information with error chains](https://rlang.r-lib.org/reference/topic-error-chaining.html) + - [Formatting messages with cli](https://rlang.r-lib.org/reference/topic-condition-formatting.html) +- [Other resources](https://github.com/rstudio-conf-2022/pkg-dev-masterclass/blob/main/materials/5-error-resources.md) from error message segment of rstudio::conf(2022) workshop "Package Development Masterclass" ## Meeting Videos diff --git a/DESCRIPTION b/DESCRIPTION @@ -19,4 +19,8 @@ Imports: DiagrammeR, palmerpenguins, deSolve, - reshape2 + reshape2, + emoji, + lobstr, + rlang, + glue diff --git a/index.Rmd b/index.Rmd @@ -17,7 +17,8 @@ description: "This is the product of the R4DS Online Learning Community's Advanc knitr::opts_chunk$set( echo = TRUE, comment = "#>", - collapse = TRUE + collapse = TRUE, + error = TRUE ) ```