topic-inject: Injecting with !!, !!!, and glue syntax

topic-injectR Documentation

Injecting with ⁠!!⁠, ⁠!!!⁠, and glue syntax

Description

The injection operators are extensions of R implemented by rlang to modify a piece of code before R processes it. There are two main families:

  • The dynamic dots operators, !!! and "{".

  • The metaprogramming operators !!, {{, and "{{". Splicing with !!! can also be done in metaprogramming context.

Dots injection

Unlike regular ..., dynamic dots are programmable with injection operators.

Splicing with ⁠!!!⁠

For instance, take a function like rbind() which takes data in .... To bind rows, you supply them as separate arguments:

rbind(a = 1:2, b = 3:4)
#>   [,1] [,2]
#> a    1    2
#> b    3    4

But how do you bind a variable number of rows stored in a list? The base R solution is to invoke rbind() with do.call():

rows <- list(a = 1:2, b = 3:4)

do.call("rbind", rows)
#>   [,1] [,2]
#> a    1    2
#> b    3    4

Functions that implement dynamic dots include a built-in way of folding a list of arguments in .... To illustrate this, we'll create a variant of rbind() that takes dynamic dots by collecting ... with list2():

rbind2 <- function(...) {
  do.call("rbind", list2(...))
}

It can be used just like rbind():

rbind2(a = 1:2, b = 3:4)
#>   [,1] [,2]
#> a    1    2
#> b    3    4

And a list of arguments can be supplied by splicing the list with !!!:

rbind2(!!!rows, c = 5:6)
#>   [,1] [,2]
#> a    1    2
#> b    3    4
#> c    5    6

Injecting names with ⁠"{"⁠

A related problem comes up when an argument name is stored in a variable. With dynamic dots, you can inject the name using glue syntax with "{":

name <- "foo"

rbind2("{name}" := 1:2, bar = 3:4)
#>     [,1] [,2]
#> foo    1    2
#> bar    3    4

rbind2("prefix_{name}" := 1:2, bar = 3:4)
#>            [,1] [,2]
#> prefix_foo    1    2
#> bar           3    4

Metaprogramming injection

Data-masked arguments support the following injection operators. They can also be explicitly enabled with inject().

Embracing with ⁠{{⁠

The embracing operator {{ is made specially for function arguments. It defuses the expression supplied as argument and immediately injects it in place. The injected argument is then evaluated in another context such as a data mask.

# Inject function arguments that might contain
# data-variables by embracing them with {{ }}
mean_by <- function(data, by, var) {
  data %>%
    dplyr::group_by({{ by }}) %>%
    dplyr::summarise(avg = mean({{ var }}, na.rm = TRUE))
}

# The data-variables `cyl` and `disp` inside the
# env-variables `by` and `var` are injected inside `group_by()`
# and `summarise()`
mtcars %>% mean_by(by = cyl, var = disp)
#> # A tibble: 3 x 2
#>     cyl   avg
#>   <dbl> <dbl>
#> 1     4  105.
#> 2     6  183.
#> 3     8  353.

Learn more about this pattern in Data mask programming patterns.

Injecting with ⁠!!⁠

Unlike !!! which injects a list of arguments, the injection operator !! (pronounced "bang-bang") injects a single object. One use case for ⁠!!⁠ is to substitute an environment-variable (created with ⁠<-⁠) with a data-variable (inside a data frame).

# The env-variable `var` contains a data-symbol object, in this
# case a reference to the data-variable `height`
var <- data_sym("disp")

# We inject the data-variable contained in `var` inside `summarise()` 
mtcars %>%
  dplyr::summarise(avg = mean(!!var, na.rm = TRUE))
#> # A tibble: 1 x 1
#>     avg
#>   <dbl>
#> 1  231.

Another use case is to inject a variable by value to avoid name collisions.

df <- data.frame(x = 1)

# This name conflicts with a column in `df`
x <- 100

# Inject the env-variable
df %>%
  dplyr::mutate(x = x / !!x)
#>      x
#> 1 0.01

Note that in most cases you don't need injection with ⁠!!⁠. For instance, the .data and .env pronouns provide more intuitive alternatives to injecting a column name and injecting a value.

Splicing with ⁠!!!⁠

The splice operator !!! of dynamic dots can also be used in metaprogramming context (inside data-masked arguments and inside inject()). For instance, we could reimplement the rbind2() function presented above using inject() instead of do.call():

rbind2 <- function(...) {
  inject(rbind(!!!list2(...)))
}

There are two things going on here. We collect ... with list2() so that the callers of rbind2() may use ⁠!!!⁠. And we use inject() so that rbind2() itself may use ⁠!!!⁠ to splice the list of arguments passed to rbind2().

Injection in other languages

Injection is known as quasiquotation in other programming languages and in computer science. expr() is similar to a quasiquotation operator and ⁠!!⁠ is the unquote operator. These terms have a rich history in Lisp languages, and live on in modern languages like Julia and Racket. In base R, quasiquotation is performed with bquote().

The main difference between rlang and other languages is that quasiquotation is often implicit instead of explicit. You can use injection operators in any defusing / quoting function (unless that function defuses its argument with a special operator like enquo0()). This is not the case in lisp languages for example where injection / unquoting is explicit and only enabled within a backquote.

See also

  • What happens if I use injection operators out of context?


tidyverse/rlang documentation built on Oct. 31, 2024, 5:35 p.m.