diff --git a/NEWS.md b/NEWS.md index 3d775e7..c630529 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # glue (development version) +* glue has a new article on how to write a wrapper function that calls `glue()` + (#281). + * glue gains a new "Getting started" article, with contributions from @stephhazlitt and @BrennanAntone (#137, #170, #332). diff --git a/vignettes/wrappers.Rmd b/vignettes/wrappers.Rmd new file mode 100644 index 0000000..6429870 --- /dev/null +++ b/vignettes/wrappers.Rmd @@ -0,0 +1,178 @@ +--- +title: "How to write a function that wraps glue" +output: rmarkdown::html_vignette +vignette: > + %\VignetteIndexEntry{How to write a function that wraps glue} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include = FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r setup} +library(glue) +``` + +Imagine that you want to call `glue()` repeatedly inside your own code (e.g. in your own package) with a non-default value for one or more arguments. +For example, maybe you anticipate producing R code where `{` and `}` have specific syntactic meaning. +Therefore, you'd prefer to use `<<` and `>>` as the opening and closing delimiters for expressions in `glue()`. + +Spoiler alert: here's the correct way to write such a wrapper: + +```{r} +my_glue <- function(..., .envir = parent.frame()) { + glue(..., .open = "<<", .close = ">>", .envir = .envir) +} +``` + +This is the key move: + +> Include `.envir = parent.frame()` as an argument of the wrapper function and pass this `.envir` to the `.envir` argument of `glue()`. + +If you'd like to know why this is the way, keep reading. +It pays off to understand this, because the technique applies more broadly than glue. +Once you recognize this setup, you'll see it in many functions in the withr, cli, and rlang packages (e.g. `withr::defer()`, `cli::cli_abort()`, `rlang::abort()`). + +## Working example + +Here's an abbreviated excerpt of the roxygen comment that generates the documentation for the starwars dataset in dplyr: + +```r +#' \describe{ +#' \item{name}{Name of the character} +#' \item{height}{Height (cm)} +#' \item{mass}{Weight (kg)} +#' \item{species}{Name of species} +#' \item{films}{List of films the character appeared in} +#' } +``` + +To produce such text programmatically, the first step might be to generate the `\item` lines from a named list of column names and descriptions. +Notice that `{` and `}` are important to the `\describe{...}` syntax, so this is an example where it is nice for glue to use different delimiters for expressions. + +Put the metadata in a suitable list: + +```{r} +sw_meta <- list( + name = "Name of the character", + height = "Height (cm)", + mass = "Weight (kg)", + species = "Name of species", + films = "List of films the character appeared in" +) +``` + +Define a custom glue wrapper and use it inside another helper that generates `\item` entries[^1]: + +[^1]: Note that delimiters `<<` and ``>>` have special meaning in knitr, so if you're generating output for RMarkdown or Quarto, you should avoid them. + +```{r} +my_glue = function(...) { + glue(..., .open = "<@", .close = "@>", .envir = parent.frame()) +} + +named_list_to_items <- function(x) { + my_glue("\\item{<@names(x)@>}{<@x@>}") +} +``` + +Apply `named_list_to_items()` to starwars metadata: + +```{r} +named_list_to_items(sw_meta) +``` + +Here's how this would fail if we did *not* handle `.envir` correctly in our wrapper function: + +```{r, error = TRUE} +my_glue_WRONG <- function(...) { + glue(..., .open = "<@", .close = "@>") +} + +named_list_to_items_WRONG <- function(x) { + my_glue_WRONG("\\item{<@names(x)@>}{<@x@>}") +} + +named_list_to_items_WRONG(sw_meta) +``` + +It can be hard to understand why `x` can't be found, when it is clearly available inside `named_list_to_items_WRONG()`. +Why isn't `x` available to `my_glue_WRONG()`? + +## Where does `glue()` evaluate code? + +What's going on? +It's time to look at the (redacted) signature of `glue()`: + +```{r, eval = FALSE} +glue(..., .envir = parent.frame(), ...) +``` + +The expressions inside a glue string are evaluated with respect to `.envir`, which defaults to the environment where `glue()` is called from. + +Everything is simple when evaluating `glue()` in the global environment: + +```{r} +x <- 0 +y <- 0 +z <- 0 + +glue("{x} {y} {z}") +``` + +Now we wrap `glue()` in our own simple function, `my_glue1()`. +Notice that `my_glue1()` does not capture its caller environment and pass that along. + +When we execute `my_glue1()` in the global environment, there's no obvious problem. + +```{r} +my_glue1 <- function(...) { + x <- 1 + glue(...) +} + +my_glue1("{x} {y} {z}") +``` + +The value of `x` is found in the execution environment of `my_glue1()`. +The values of `y` and `z` are found in the global environment. +Importantly, this is because that is the environment where `my_glue1()` is defined, not because that is where `my_glue1()` is called. + +However, if we call our `my_glue1()` inside another function, we see that all is not well. + +```{r} +my_glue2 <- function(...) { + x <- 2 + y <- 2 + my_glue1(...) +} + +my_glue2("{x} {y} {z}") +``` + +Why do `x` and `y` not have the value 2? +Because `my_glue1()` and its eventual call to `glue()` have no access to the execution environment of `my_glue2()`, which is the caller environment of `my_glue1()`. + +If you want your glue wrapper to behave like `glue()` itself and to work as expected inside other functions, make sure it captures its caller environment and passes that along to `glue()`. + +```{r} +my_glue3 <- function(..., .envir = parent.frame()) { + x <- 3 + glue(..., .envir = .envir) +} + +my_glue3("{x} {y} {z}") + +my_glue4 <- function(...) { + x <- 4 + y <- 4 + my_glue3(...) +} + +my_glue4("{x} {y} {z}") +```