diff --git a/_freeze/posts/2024-03-12-eclipse/index/execute-results/html.json b/_freeze/posts/2024-03-12-eclipse/index/execute-results/html.json index e6cbcc5..563c515 100644 --- a/_freeze/posts/2024-03-12-eclipse/index/execute-results/html.json +++ b/_freeze/posts/2024-03-12-eclipse/index/execute-results/html.json @@ -1,7 +1,7 @@ { - "hash": "a853ad31e3ef77c77b5920bc096a61bc", + "hash": "258f1134b77defd1cee4892465a381ff", "result": { - "markdown": "---\ntitle: \"Ellipses, eclipses and bulletses\"\ndate: 2024-03-12\nslug: \"eclipse\"\nimage: resources/cli-eclipse.png\ncategories:\n - cli\n - r\n - rlang\n---\n\n\n![](resources/cli-eclipse.png){fig.alt=\"An R function called add_one is run in an RStudio console with a string passed as the only argument. The output reads 'Error in add_one, x must be of class numeric, you provided x with class character.' The output is coloured and there are symbols at the start of certain lines, like an exclamation point and red cross. It ends with backtrace to see the error was from add_one in the global environment. Also there's a creeppy emoji sun that's not-quite obscured by a creepy emoji moon.\"}\n\n## tl;dr\n\nYou can use [{cli}](https://cli.r-lib.org/) and [{rlang}](https://rlang.r-lib.org/) to help create a helpful error-handling function that can prevent an eclipse.\n\n## Check one two\n\nI've been building an 'error helper' function[^helper] called `check_class()`. You put it inside another function to check if the user has supplied arguments of the expected class. Surprise.\n\nI did this to provide richer, more informative error output compared to a simple `if () stop()`. But it has a few features I wanted to record here for my own reference.\n\nIn particular, `check_class()`:\n\n* uses {rlang} to handle multiple inputs to the dots (`...`) argument\n* uses {rlang} to avoid an 'error-handling eclipse'\n* uses {cli} for pretty error messaging\n* can handle multiple errors and build bulleted {cli} lists from them\n\nMaybe you'll find something useful here too.\n\n## A classy check\n\n\n::: {.cell}\n\n:::\n\n\n### Examples\n\nHere are some simple examples of inserting `check_class()` inside another function. The simple function `add_one()` expects a numeric value. What happens if you supply a string?\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_one <- function(x) {\n check_class(x, .expected_class = \"numeric\")\n x + 1\n}\n\nadd_one(\"text\")\n```\n\n::: {.cell-output .cell-output-error}\n```\nError in `add_one()`:\n! `x` must be of class \n✖ You provided:\n• `x` with class \n```\n:::\n:::\n\n\nYou get a user-friendly output that tells you the problem and where you went wrong. It doesn't render in this blog post, but in supported consoles the output will be coloured and you'll get a backtrace to say where the error occurred. See the image at the top of the post for an example.\n\nYou can provide an arbitrary number of values to `check_class()` for assessment. The example below takes three arguments that should all be numeric. What happens if we supply three objects of the wrong type?\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmultiply_and_raise <- function(x, y, z) {\n check_class(x, y, z, .expected_class = \"numeric\")\n x * y ^ z\n}\n\nmultiply_and_raise(list(), data.frame(), matrix())\n```\n\n::: {.cell-output .cell-output-error}\n```\nError in `multiply_and_raise()`:\n! `x`, `y`, and `z` must be of class \n✖ You provided:\n• `x` with class \n• `y` with class \n• `z` with class \n```\n:::\n:::\n\n\nThe output now shows each failure as separate bullet points so it's clear where we made the error and what the problem was.\n\n## Function\n\nBelow is what the `check_class()` function actually looks like. I've added some comments to explain what's happening at each step. For demo purposes, the function is equipped to check for numeric and character classes only, but you could expand the `switch()` statement for other classes too.\n\n\n::: {.cell}\n\n```{.r .cell-code}\n#' Check Class of Argument Inputs\n#' @param ... Objects to be checked for class.\n#' @param .expected_class Character. The name of the class against which objects\n#' should be checked.\n#' @param .call The environment in which this function is to be called.\n#' @noRd\ncheck_class <- function(\n ...,\n .expected_class = c(\"numeric\", \"character\"),\n .call = rlang::caller_env()\n) {\n \n .expected_class = match.arg(.expected_class) # ensures 'numeric'/'character'\n \n args <- rlang::dots_list(..., .named = TRUE) # collect dots values\n \n # Check each value against expected class\n args_are_class <- lapply(\n args,\n function(arg) {\n switch(\n .expected_class,\n numeric = is.numeric(arg),\n character = is.character(arg),\n )\n }\n )\n \n # Isolate values that have wrong class\n fails_names <- names(Filter(isFALSE, args_are_class))\n \n if (length(fails_names) > 0) {\n \n # Prepare variables with failure information\n fails <- args[names(args) %in% fails_names]\n fails_classes <- sapply(fails, class)\n \n # Build a bulleted {cli}-styled vector of the failures\n fails_bullets <- setNames(\n paste0(\n \"{.var \", names(fails_classes), \"} with class {.cls \",\n fails_classes, \"}\"\n ),\n rep(\"*\", length(fails_classes)) # name with bullet point symbol\n )\n \n # Raise the error, printed nicely in {cli} style\n cli::cli_abort(\n message = c(\n \"{.var {fails_names}} must be of class {.cls {(.expected_class)}}\",\n x = \"You provided:\", fails_bullets\n ),\n call = .call # environment of parent function, not check_class() itself\n )\n }\n \n}\n```\n:::\n\n\nAnd now to explain in a bit more depth those features I mentioned.\n\n## Features\n\n### Ellipses\n\nWhen a function has a dots (`...`) argument, it means you can pass an arbitrary number of objects to be captured. Consider `paste(\"You\", \"smell\")` (two values), `paste(\"You\", \"smell\", \"wonderful\")` (three), etc, or how you can provide an arbitrary number of column names to `dplyr::select()`.\n\nThe first argument to `check_class()` is `...`. You pass to it as many values as you need to assess for an expected class. So the function `add_one(x)` would contain a call to `check_class(x, \"numeric\")` (one argument to check), while `multiply(x, y)` could take `check_class(x, y, \"numeric\")` (two)[^dot].\n\nI've used the {rlang} package's `dots_list()` function to collect the dots elements into a list. The `.named = TRUE` argument names each element, so we can pinpoint the errors and report them to the user.\n\nI have collaborators, so readability of the code is important. I think `rlang::dots_list()` is more readable than the base approach, which is something like:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nargs <- list(...)\narg_names <- as.character(substitute(...()))\nnames(args) <- arg_names\n```\n:::\n\n\n### Eclipses\n\nSo: you put `check_class()` inside another function. This causes a problem: errors will be reported to the user as having been raised by `check_class()`, but it's an internal function that they'll never see. It would be better to report the error has having originated from the parent function instead.\n\nThis obfuscation, this 'code smell', has been nicknamed an 'error-handling eclipse' by Nick Tierney, whose [blog post](https://www.njtierney.com/post/2023/12/06/long-errors-smell/) was extremely well-timed for when I was writing `check_class()`.\n\nIn short, you can record with `rlang::caller_env()` the environment in which the `check_class()` function was used. You can hand that to the `call` function of `cli::cli_abort()`, which `check_class()` uses to build and report error messages. This means the error is reported from the function enclosing `check_class()`, not from `check_class()` itself.\n\nFor example, here's an example `report_env()` function, which prints the environment in which it's called. Since this is being run in the global environment, the global environment will be printed.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nremove(list = ls()) # clear the global environment\nreport_env <- function(env = rlang::caller_env()) rlang::env_print(env)\nreport_env()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nParent: \nBindings:\n• report_env: \n• .main: \n```\n:::\n:::\n\n\nIf we nest `report_env()` inside another function then the reported environment is of that enclosing function (expressed here as its bytecode), which itself is nested in its parent (global) environment.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nreport_env_2 <- function() report_env()\nreport_env_2()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nParent: \n```\n:::\n:::\n\n\nSee the image at the top of this post, which shows the backtrace as having originated from the enclosing `add_one()` function rather than the `check_class()` call within it.\n\n### Bulletses\n\n[The {cli} package](https://github.com/r-lib/cli) lets you build rich user interfaces for your functions[^hyperlink]. This is great for composing informative warning and error messages for the user.\n\nLet's focus on a simplified example of `cli::cli_abort()`, which is like the {cli} equivalent of `stop()`. Let's pretend we passed a character vector when it should have been numeric. \n\nTo the `message` argument you provide a named vector, where the name will be printed as a symbol in the output. This will be a yellow exclamation point for `cli_abort()` by default, which draws attention to the exact error. The name 'x' prints as a red cross to indicate what the user did wrong. \n\nYou can also use {glue} syntax in {cli} to evaluate variables. But {cli} goes one further: it has special syntax to provide consistent mark-up to bits of text. For example, `\"{.var x}\"` will print with surrounding backticks and `\"{.cls numeric}\"` will print in blue with surrounding less/greater than symbols.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nfail_class <- \"character\" \ncli::cli_abort(\n message = c(\n \"{.var x} must be of class {.cls numeric}\",\n x = \"You provided class {.cls {fail_class}}\"\n )\n)\n```\n\n::: {.cell-output .cell-output-error}\n```\nError:\n! `x` must be of class \n✖ You provided class \n```\n:::\n:::\n\n\nAgain, see an example in the image at the top of the post.\n\nSince `check_class()` can take multiple values via the dots, we can construct an individual report for each failing element. {cli} will automatically turn each of these constructed lines into a bullet point in the printed output if we name them with an asterisk, which is pretty neat.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nexpected_class <- \"numeric\"\nfails <- list(x = \"character\", y = \"list\")\nfails_names <- names(fails)\n\nfails_bullets <- setNames(\n paste0(\"{.var \", fails_names, \"} with class {.cls \", fails, \"}\"),\n rep(\"*\", length(fails))\n)\n\ncli::cli_abort(\n message = c(\n \"{.var {fails_names}} must be of class {.cls {expected_class}}\",\n x = \"You provided:\", fails_bullets\n )\n)\n```\n\n::: {.cell-output .cell-output-error}\n```\nError:\n! `x` and `y` must be of class \n✖ You provided:\n• `x` with class \n• `y` with class \n```\n:::\n:::\n\n\nPew pew pew.\n\n#### Test\n\nHere's a cheeky bonus if you're wondering how to test for {cli} messages: you can use `cli::test_that_cli()` to test the output against [an earlier snapshot](https://testthat.r-lib.org/articles/snapshotting.html).\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncli::test_that_cli(\"prints expected error\", {\n testthat::local_edition(3) # only works with {testthat} 3e\n testthat::expect_snapshot({\n check_class(x = 1, y = \"x\", .expected_class = \"numeric\")\n })\n})\n```\n:::\n\n\n## Error-helper help?\n\nIs this horribly overengineered? What is your approach to creating friendly and actionable error messages for your users?\n\n### Environment {.appendix}\n\n
Session info\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n```\nLast rendered: 2024-03-12 21:02:41 GMT\n```\n:::\n\n::: {.cell-output .cell-output-stdout}\n```\nR version 4.3.1 (2023-06-16)\nPlatform: aarch64-apple-darwin20 (64-bit)\nRunning under: macOS Ventura 13.2.1\n\nMatrix products: default\nBLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib \nLAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0\n\nlocale:\n[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8\n\ntime zone: Europe/London\ntzcode source: internal\n\nattached base packages:\n[1] stats graphics grDevices utils datasets methods base \n\nloaded via a namespace (and not attached):\n [1] digest_0.6.33 utf8_1.2.4 fastmap_1.1.1 xfun_0.41 \n [5] glue_1.7.0 knitr_1.45 htmltools_0.5.6.1 rmarkdown_2.25 \n [9] lifecycle_1.0.4 cli_3.6.2 fansi_1.0.6 vctrs_0.6.5 \n[13] compiler_4.3.1 rstudioapi_0.15.0 tools_4.3.1 evaluate_0.23 \n[17] pillar_1.9.0 yaml_2.3.8 rlang_1.1.3 jsonlite_1.8.7 \n[21] htmlwidgets_1.6.2\n```\n:::\n:::\n\n
\n\n[^helper]: {rlang} has [some helpful documentation on error helpers](https://rlang.r-lib.org/reference/topic-error-call.html), which you can find by typing `` ?rlang::`topic-error-call` `` into the console\n[^dot]: I've used a dot-prefix for the remaining `check_class()` arguments, which reduces the chance of a clash with user-supplied values to the dots. This is recommended in [the Tidy Design Principles book](https://design.tidyverse.org/dots-prefix.html).\n[^hyperlink]: [I wrote about {cli} in an earlier post](https://www.rostrum.blog/posts/2023-09-17-choosethis/), where I explored its ability to generate hyperlinks in the R console. I used it for fun (to build a choose-your-own-adventure in the console), but it can be useful for things like opening a file at the exact line where a test failure occurred.", + "markdown": "---\ntitle: \"Ellipses, eclipses and bulletses\"\ndate: 2024-03-12\nslug: \"eclipse\"\nimage: resources/cli-eclipse.png\ncategories:\n - cli\n - r\n - rlang\n---\n\n\n![](resources/cli-eclipse.png){fig.alt=\"An R function called add_one is run in an RStudio console with a string passed as the only argument. The output reads 'Error in add_one, x must be of class numeric, you provided x with class character.' The output is coloured and there are symbols at the start of certain lines, like an exclamation point and red cross. It ends with backtrace to see the error was from add_one in the global environment. Also there's a creeppy emoji sun that's not-quite obscured by a creepy emoji moon.\"}\n\n## tl;dr\n\nYou can use [{cli}](https://cli.r-lib.org/) and [{rlang}](https://rlang.r-lib.org/) to help create a helpful error-handling function that can prevent an eclipse.\n\n## Check one two\n\nI've been building an 'error helper' function[^helper] called `check_class()`. You put it inside another function to check if the user has supplied arguments of the expected class. Surprise.\n\nI did this to provide richer, more informative error output compared to a simple `if () stop()`. But it has a few features I wanted to record here for my own reference.\n\nIn particular, `check_class()`:\n\n* uses {rlang} to handle multiple inputs to the dots (`...`) argument\n* uses {rlang} to avoid an 'error-handling eclipse'\n* uses {cli} for pretty error messaging\n* can handle multiple errors and build bulleted {cli} lists from them\n\nMaybe you'll find something useful here too.\n\n## A classy check\n\n\n::: {.cell}\n\n:::\n\n\n### Examples\n\nHere are some simple examples of inserting `check_class()` inside another function. The simple function `add_one()` expects a numeric value. What happens if you supply a string?\n\n\n::: {.cell}\n\n```{.r .cell-code}\nadd_one <- function(x) {\n check_class(x, .expected_class = \"numeric\")\n x + 1\n}\n\nadd_one(\"text\")\n```\n\n::: {.cell-output .cell-output-error}\n```\nError in `add_one()`:\n! `x` must be of class \n✖ You provided:\n• `x` with class \n```\n:::\n:::\n\n\nYou get a user-friendly output that tells you the problem and where you went wrong. It doesn't render in this blog post, but in supported consoles the output will be coloured and you'll get a backtrace to say where the error occurred. See the image at the top of the post for an example.\n\nYou can provide an arbitrary number of values to `check_class()` for assessment. The example below takes three arguments that should all be numeric. What happens if we supply three objects of the wrong type?\n\n\n::: {.cell}\n\n```{.r .cell-code}\nmultiply_and_raise <- function(x, y, z) {\n check_class(x, y, z, .expected_class = \"numeric\")\n x * y ^ z\n}\n\nmultiply_and_raise(list(), data.frame(), matrix())\n```\n\n::: {.cell-output .cell-output-error}\n```\nError in `multiply_and_raise()`:\n! `x`, `y`, and `z` must be of class \n✖ You provided:\n• `x` with class \n• `y` with class \n• `z` with class \n```\n:::\n:::\n\n\nThe output now shows each failure as separate bullet points so it's clear where we made the error and what the problem was.\n\n## Function\n\nBelow is what the `check_class()` function actually looks like. I've added some comments to explain what's happening at each step. For demo purposes, the function is equipped to check for numeric and character classes only, but you could expand the `switch()` statement for other classes too.\n\n\n::: {.cell}\n\n```{.r .cell-code}\n#' Check Class of Argument Inputs\n#' @param ... Objects to be checked for class.\n#' @param .expected_class Character. The name of the class against which objects\n#' should be checked.\n#' @param .call The environment in which this function is to be called.\n#' @noRd\ncheck_class <- function(\n ...,\n .expected_class = c(\"numeric\", \"character\"),\n .call = rlang::caller_env()\n) {\n \n .expected_class = match.arg(.expected_class) # ensures 'numeric'/'character'\n \n args <- rlang::dots_list(..., .named = TRUE) # collect dots values\n \n # Check each value against expected class\n args_are_class <- lapply(\n args,\n function(arg) {\n switch(\n .expected_class,\n numeric = is.numeric(arg),\n character = is.character(arg),\n )\n }\n )\n \n # Isolate values that have wrong class\n fails_names <- names(Filter(isFALSE, args_are_class))\n \n if (length(fails_names) > 0) {\n \n # Prepare variables with failure information\n fails <- args[names(args) %in% fails_names]\n fails_classes <- sapply(fails, class)\n \n # Build a bulleted {cli}-styled vector of the failures\n fails_bullets <- setNames(\n paste0(\n \"{.var \", names(fails_classes), \"} with class {.cls \",\n fails_classes, \"}\"\n ),\n rep(\"*\", length(fails_classes)) # name with bullet point symbol\n )\n \n # Raise the error, printed nicely in {cli} style\n cli::cli_abort(\n message = c(\n \"{.var {fails_names}} must be of class {.cls {(.expected_class)}}\",\n x = \"You provided:\", fails_bullets\n ),\n call = .call # environment of parent function, not check_class() itself\n )\n }\n \n}\n```\n:::\n\n\nAnd now to explain in a bit more depth those features I mentioned.\n\n## Features\n\n### Ellipses\n\nWhen a function has a dots (`...`) argument, it means you can pass an arbitrary number of objects to be captured. Consider `paste(\"You\", \"smell\")` (two values), `paste(\"You\", \"smell\", \"wonderful\")` (three), etc, or how you can provide an arbitrary number of column names to `dplyr::select()`.\n\nThe first argument to `check_class()` is `...`. You pass to it as many values as you need to assess for an expected class. So the function `add_one(x)` would contain within it a call to `check_class(x, .expected_class = \"numeric\")` (one argument to check), while `multiply(x, y)` would accept `check_class(x, y, .expected_class = \"numeric\")` (two)[^dot].\n\nI've used the {rlang} package's `dots_list()` function to collect the dots elements into a list. The `.named = TRUE` argument names each element, so we can pinpoint the errors and report them to the user.\n\nI have collaborators, so readability of the code is important. I think `rlang::dots_list()` is more readable than the base approach, which is something like:\n\n\n::: {.cell}\n\n```{.r .cell-code}\nargs <- list(...)\narg_names <- as.character(substitute(...()))\nnames(args) <- arg_names\n```\n:::\n\n\n### Eclipses\n\nSo: you put `check_class()` inside another function. This causes a problem: errors will be reported to the user as having been raised by `check_class()`, but it's an internal function that they'll never see. It would be better to report the error has having originated from the parent function instead.\n\nThis obfuscation, this 'code smell', has been nicknamed an 'error-handling eclipse' by Nick Tierney, whose [blog post](https://www.njtierney.com/post/2023/12/06/long-errors-smell/) was extremely well-timed for when I was writing `check_class()`.\n\nIn short, you can record with `rlang::caller_env()` the environment in which the `check_class()` function was used. You can hand that to the `call` function of `cli::cli_abort()`, which `check_class()` uses to build and report error messages. This means the error is reported from the function enclosing `check_class()`, not from `check_class()` itself.\n\nFor example, here's an example `report_env()` function, which prints the environment in which it's called. Since this is being run in the global environment, the global environment will be printed.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nremove(list = ls()) # clear the global environment\nreport_env <- function(env = rlang::caller_env()) rlang::env_print(env)\nreport_env()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nParent: \nBindings:\n• report_env: \n• .main: \n```\n:::\n:::\n\n\nIf we nest `report_env()` inside another function then the reported environment is of that enclosing function (expressed here as its bytecode), which itself is nested in its parent (global) environment.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nreport_env_2 <- function() report_env()\nreport_env_2()\n```\n\n::: {.cell-output .cell-output-stdout}\n```\n\nParent: \n```\n:::\n:::\n\n\nSee the image at the top of this post, which shows the backtrace as having originated from the enclosing `add_one()` function rather than the `check_class()` call within it.\n\n### Bulletses\n\n[The {cli} package](https://github.com/r-lib/cli) lets you build rich user interfaces for your functions[^hyperlink]. This is great for composing informative warning and error messages for the user.\n\nLet's focus on a simplified example of `cli::cli_abort()`, which is like the {cli} equivalent of `stop()`. Let's pretend we passed a character vector when it should have been numeric. \n\nTo the `message` argument you provide a named vector, where the name will be printed as a symbol in the output. This will be a yellow exclamation point for `cli_abort()` by default, which draws attention to the exact error. The name 'x' prints as a red cross to indicate what the user did wrong. \n\nYou can also use {glue} syntax in {cli} to evaluate variables. But {cli} goes one further: it has special syntax to provide consistent mark-up to bits of text. For example, `\"{.var x}\"` will print with surrounding backticks and `\"{.cls numeric}\"` will print in blue with surrounding less/greater than symbols.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nfail_class <- \"character\" \ncli::cli_abort(\n message = c(\n \"{.var x} must be of class {.cls numeric}\",\n x = \"You provided class {.cls {fail_class}}\"\n )\n)\n```\n\n::: {.cell-output .cell-output-error}\n```\nError:\n! `x` must be of class \n✖ You provided class \n```\n:::\n:::\n\n\nAgain, see an example in the image at the top of the post.\n\nSince `check_class()` can take multiple values via the dots, we can construct an individual report for each failing element. {cli} will automatically turn each of these constructed lines into a bullet point in the printed output if we name them with an asterisk, which is pretty neat.\n\n\n::: {.cell}\n\n```{.r .cell-code}\nexpected_class <- \"numeric\"\nfails <- list(x = \"character\", y = \"list\")\nfails_names <- names(fails)\n\nfails_bullets <- setNames(\n paste0(\"{.var \", fails_names, \"} with class {.cls \", fails, \"}\"),\n rep(\"*\", length(fails))\n)\n\ncli::cli_abort(\n message = c(\n \"{.var {fails_names}} must be of class {.cls {expected_class}}\",\n x = \"You provided:\", fails_bullets\n )\n)\n```\n\n::: {.cell-output .cell-output-error}\n```\nError:\n! `x` and `y` must be of class \n✖ You provided:\n• `x` with class \n• `y` with class \n```\n:::\n:::\n\n\nPew pew pew.\n\n#### Test\n\nHere's a cheeky bonus if you're wondering how to test for {cli} messages: you can use `cli::test_that_cli()` to test the output against [an earlier snapshot](https://testthat.r-lib.org/articles/snapshotting.html).\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncli::test_that_cli(\"prints expected error\", {\n testthat::local_edition(3) # only works with {testthat} 3e\n testthat::expect_snapshot({\n check_class(x = 1, y = \"x\", .expected_class = \"numeric\")\n })\n})\n```\n:::\n\n\n## Error-helper help?\n\nIs this horribly overengineered? What is your approach to creating friendly and actionable error messages for your users?\n\n### Environment {.appendix}\n\n
Session info\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n```\nLast rendered: 2024-04-10 08:54:28 BST\n```\n:::\n\n::: {.cell-output .cell-output-stdout}\n```\nR version 4.3.1 (2023-06-16)\nPlatform: aarch64-apple-darwin20 (64-bit)\nRunning under: macOS Ventura 13.2.1\n\nMatrix products: default\nBLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib \nLAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0\n\nlocale:\n[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8\n\ntime zone: Europe/London\ntzcode source: internal\n\nattached base packages:\n[1] stats graphics grDevices utils datasets methods base \n\nloaded via a namespace (and not attached):\n [1] digest_0.6.35 utf8_1.2.4 fastmap_1.1.1 xfun_0.43 \n [5] glue_1.7.0 knitr_1.45 htmltools_0.5.8 rmarkdown_2.26 \n [9] lifecycle_1.0.4 cli_3.6.2 fansi_1.0.6 vctrs_0.6.5 \n[13] compiler_4.3.1 rstudioapi_0.16.0 tools_4.3.1 evaluate_0.23 \n[17] pillar_1.9.0 yaml_2.3.8 rlang_1.1.3 jsonlite_1.8.8 \n[21] htmlwidgets_1.6.2\n```\n:::\n:::\n\n
\n\n[^helper]: {rlang} has [some helpful documentation on error helpers](https://rlang.r-lib.org/reference/topic-error-call.html), which you can find by typing `` ?rlang::`topic-error-call` `` into the console\n[^dot]: I've used a dot-prefix for the remaining `check_class()` arguments, which reduces the chance of a clash with user-supplied values to the dots. This is recommended in [the Tidy Design Principles book](https://design.tidyverse.org/dots-prefix.html).\n[^hyperlink]: [I wrote about {cli} in an earlier post](https://www.rostrum.blog/posts/2023-09-17-choosethis/), where I explored its ability to generate hyperlinks in the R console. I used it for fun (to build a choose-your-own-adventure in the console), but it can be useful for things like opening a file at the exact line where a test failure occurred.", "supporting": [], "filters": [ "rmarkdown/pagebreak.lua" diff --git a/_site/index.html b/_site/index.html index 261e728..9375930 100644 --- a/_site/index.html +++ b/_site/index.html @@ -210,7 +210,7 @@
-
+
-
+
-
+
diff --git a/_site/index.xml b/_site/index.xml index 9864fe0..8820015 100644 --- a/_site/index.xml +++ b/_site/index.xml @@ -1557,7 +1557,7 @@ font-style: inherit;"># environment of parent function, not check_class() itself

Ellipses

When a function has a dots (...) argument, it means you can pass an arbitrary number of objects to be captured. Consider paste("You", "smell") (two values), paste("You", "smell", "wonderful") (three), etc, or how you can provide an arbitrary number of column names to dplyr::select().

-

The first argument to check_class() is .... You pass to it as many values as you need to assess for an expected class. So the function add_one(x) would contain a call to check_class(x, "numeric") (one argument to check), while multiply(x, y) could take check_class(x, y, "numeric") (two)2.

+

The first argument to check_class() is .... You pass to it as many values as you need to assess for an expected class. So the function add_one(x) would contain within it a call to check_class(x, .expected_class = "numeric") (one argument to check), while multiply(x, y) would accept check_class(x, y, .expected_class = "numeric") (two)2.

I’ve used the {rlang} package’s dots_list() function to collect the dots elements into a list. The .named = TRUE argument names each element, so we can pinpoint the errors and report them to the user.

I have collaborators, so readability of the code is important. I think rlang::dots_list() is more readable than the base approach, which is something like:

@@ -1637,7 +1637,7 @@ font-style: inherit;">report_env() background-color: null; font-style: inherit;">report_env_2()
-
<environment: 0x11f0af038>
+
<environment: 0x10b25d0a0>
 Parent: <environment: global>
@@ -1821,7 +1821,7 @@ Session info
-
Last rendered: 2024-03-12 21:02:41 GMT
+
Last rendered: 2024-04-10 08:54:28 BST
R version 4.3.1 (2023-06-16)
@@ -1842,11 +1842,11 @@ attached base packages:
 [1] stats     graphics  grDevices utils     datasets  methods   base     
 
 loaded via a namespace (and not attached):
- [1] digest_0.6.33     utf8_1.2.4        fastmap_1.1.1     xfun_0.41        
- [5] glue_1.7.0        knitr_1.45        htmltools_0.5.6.1 rmarkdown_2.25   
+ [1] digest_0.6.35     utf8_1.2.4        fastmap_1.1.1     xfun_0.43        
+ [5] glue_1.7.0        knitr_1.45        htmltools_0.5.8   rmarkdown_2.26   
  [9] lifecycle_1.0.4   cli_3.6.2         fansi_1.0.6       vctrs_0.6.5      
-[13] compiler_4.3.1    rstudioapi_0.15.0 tools_4.3.1       evaluate_0.23    
-[17] pillar_1.9.0      yaml_2.3.8        rlang_1.1.3       jsonlite_1.8.7   
+[13] compiler_4.3.1    rstudioapi_0.16.0 tools_4.3.1       evaluate_0.23    
+[17] pillar_1.9.0      yaml_2.3.8        rlang_1.1.3       jsonlite_1.8.8   
 [21] htmlwidgets_1.6.2
diff --git a/_site/posts/2022-05-01-dungeon/index.html b/_site/posts/2022-05-01-dungeon/index.html index a998f7a..1a0ffff 100644 --- a/_site/posts/2022-05-01-dungeon/index.html +++ b/_site/posts/2022-05-01-dungeon/index.html @@ -246,7 +246,7 @@ }; // Store cell data -globalThis.qwebrCellDetails = [{"code":"webr::install(\"r.oguelike\", repos = \"https://matt-dray.r-universe.dev\")\nwebr::install(\"crayon\")","id":1,"options":{"context":"setup"}},{"code":"r.oguelike::generate_dungeon(\n iterations = 4,\n n_row = 25,\n n_col = 40,\n n_rooms = 5,\n is_snake = FALSE,\n is_organic = TRUE\n)","id":2,"options":{"label":"generate-dungeon","context":"interactive"}}]; +globalThis.qwebrCellDetails = [{"options":{"context":"setup"},"code":"webr::install(\"r.oguelike\", repos = \"https://matt-dray.r-universe.dev\")\nwebr::install(\"crayon\")","id":1},{"options":{"context":"interactive","label":"generate-dungeon"},"code":"r.oguelike::generate_dungeon(\n iterations = 4,\n n_row = 25,\n n_col = 40,\n n_rooms = 5,\n is_snake = FALSE,\n is_organic = TRUE\n)","id":2}];