Day 2 Session 2: Managing Side Effects
Invalid Date
At the end of this section you will be able to:
Sometimes, error messages can be cryptic:
seq[10]Error in seq[10] : object of type 'closure' is not subsettable
An effective error condition has:
cli::cli_abort()
{cli} package (Csárdi 2022)
Prefer:
Over:
Finding simplest set of predicates is just as challenging as finding the “right” names for functions and arguments.
Content:
Formatting:
{cli} provides powerful formatting tools:
{.var y}
{.val {y}}
see cli inline-markup for more details.
A developer, calling your function, can use the class name to handle the error, if they want.
Convention:
"{package}_error_{description}"This “stuff” is also available to an error handler.
Provide the data that went into the predicate.
Provide name of variable, e.g. y = y.
Avoid reserved names: message, class, call, body, footer, trace, parent, use_cli_format.
If there will be an error, surface it quickly.
Validating the arguments to a function is one way to do this.
For example:
Questions like these can be generalized into functions:
You may be familiar with match.arg():
rlang::arg_match() does the same thing, but:
Designed for capturing side-effects:
dplyr::glimpse() is a useful shortcutBe careful about accepting changes (don’t just accept).
Can be temperamental - not run on CRAN.
We will go through, using examples.
"2.2.1"
Implement validator-functions:
country valid?btt22::btt_reset_hard("2.2.1")
Get new files, btt22::btt_get("2.2.1"):
validate.Rtest-validate.Rusethis::use_package("cli")
rlang::arg_match()
👉 devtools::load_all(), then:
uss_get_matches("italy") 🎉uss_get_matches("tatooine") 🎉uss_get_matches("ita") 🤔get-matches.R:
match.arg() with rlang::arg_match()
test-get-matches.R:
2.2.1, then repeat 👉usethis::use_package("cli")
review validate_data_frame() (call argument)
build constructor for validate_cols():
{.field {cols}} may be useful2.2.1 tests in test-validate.R, as you gohold off on snapshot test (we’ll do together)
uss_make_matches():
matches.Rcols_engsoc() may be usefulLeave no footprints.
Leave the global state how you found it, avoid surprises later:
Loading {devtools} in .Rprofile changes the global state.
When we hit the “Knit” button, or the Quarto “Render” button:
.Rprofile
The {withr} package (Hester et al. 2022) gives us tools to:
Useful family of functions:
withr::local_*()CRAN is (rightly) particular about “leave no footprints”.
You may need:
withr::local_options(): change an optionwithr::local_dir(): change the working directorywithr::local_tempfile(): path to a temporary fileUseful in testthat code and in R code.
not_runif <- function(n, min = 0, max = 1) {
withr::local_seed(314)
runif(n, min = min, max = max)
}usethis::use_test("validate")
change:
expect_identical(error_condition$cols_req, "foo")to:
expect_identical(error_condition$cols_re, "foo")rerun tests 🤔
By default, $ accepts partial matching.
We want stricter testing, so we need to set some options.
"2.2.2"
Turn off $ partial matching when testing.
btt22::btt_reset_hard("2.2.2")
Get new files, btt22::btt_get("2.2.2"):
utils-testing.Rusethis::use_package("withr")
local_warn_partial_match()In essence, this sets (temporarily) TRUE:
options("warnPartialMatchDollar")options("warnPartialMatchArgs")options("warnPartialMatchAttr")With a couple of wrinkles:
NULL as FALSE."2.2.2", continuedusethis::use_test("validate")
add local_warn_partial_match() to top of blocks.
getOption("warnPartialMatchDollar")
change:
expect_identical(error_condition$cols_re, "foo")devtools::test() 🎉
getOption("warnPartialMatchDollar")
change back to $cols_req
"2.2.3"
Implement testing with side effects
btt22::btt_reset_hard("2.2.3")
No new files.
add local_warn_partial_match() to all test-blocks.
test-matches.R:
expect_error() calls for data-frame and columns (use class argument).expect_snapshot(dplyr::glimpse(italy))The most-common side-effect is an error. Good design:
class using naming convention, and additional informationUse snapshot tests to capture side-effects.
Use withr::local_*() functions to “leave no footprints”.
Brian Beckman on monads (Ian watched at least ten times, learned something each time):
tidyverse is a monoidal system
maybe monads aren’t the thing, but the functional-programming learned along the way