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:

Dots injection

Unlike regular ..., [dynamic dots][dyn-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)

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)

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)

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

rbind2(!!!rows, c = 5:6)

Injecting names with \verb{"{"}

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 r link("'{'"):

name <- "foo"

rbind2("{name}" := 1:2, bar = 3:4)

rbind2("prefix_{name}" := 1:2, bar = 3:4)

Metaprogramming injection

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

Embracing with {{

The embracing operator r link("{{") is made specially for function arguments. It [defuses][topic-defuse] 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][topic-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)

Learn more about this pattern in r link("topic_data_mask_programming").

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))

Another use case is to inject a variable by value to avoid [name collisions][topic-data-mask-ambiguity].

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)

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][topic-data-mask] 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



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