R: Splice operator !!!

splice-operator {rlang} | R Documentation

## Splice operator `!!!`

### Description

The splice operator `!!!` implemented in dynamic dots injects a list of arguments into a function call. It belongs to the family of injection operators and provides the same functionality as `do.call()`.

The two main cases for splice injection are:

- Turning a list of inputs into distinct arguments. This is especially useful with functions that take data in `...`, such as `base::rbind()`.

      dfs <- list(mtcars, mtcars)
      inject(rbind(!!!mtcars))

- Injecting defused expressions like symbolised column names.

  For tidyverse APIs, this second case is no longer as useful since dplyr 1.0 and the `across()` operator.

### Where does `!!!` work?

`!!!` does not work everywhere, you can only use it within certain special functions:

- Functions taking dynamic dots like `list2()`.

- Functions taking defused and data-masked arguments, which are dynamic by default.

- Inside `inject()`.

Most tidyverse functions support `!!!` out of the box. With base functions you need to use `inject()` to enable `!!!`.

Using the operator out of context may lead to incorrect results, see What happens if I use injection operators out of context?.

### Splicing a list of arguments

Take a function like `base::rbind()` that takes data in `...`. This sort of functions takes a variable number of arguments.

    df1 <- data.frame(x = 1)
    df2 <- data.frame(x = 2)

    rbind(df1, df2)
    #>   x
    #> 1 1
    #> 2 2

Passing individual arguments is only possible for a fixed amount of arguments. When the arguments are in a list whose length is variable (and potentially very large), we need a programmatic approach like the splicing syntax `!!!`:

    dfs <- list(df1, df2)

    inject(rbind(!!!dfs))
    #>   x
    #> 1 1
    #> 2 2

Because `rbind()` is a base function we used `inject()` to explicitly enable `!!!`. However, many functions implement dynamic dots with `!!!` implicitly enabled out of the box.

    tidyr::expand_grid(x = 1:2, y = c("a", "b"))
    #> # A tibble: 4 × 2
    #>       x y
    #>   <int> <chr>
    #> 1     1 a
    #> 2     1 b
    #> 3     2 a
    #> 4     2 b

    xs <- list(x = 1:2, y = c("a", "b"))
    tidyr::expand_grid(!!!xs)
    #> # A tibble: 4 × 2
    #>       x y
    #>   <int> <chr>
    #> 1     1 a
    #> 2     1 b
    #> 3     2 a
    #> 4     2 b

Note how the expanded grid has the right column names. That's because we spliced a named list. Splicing causes each name of the list to become an argument name.

    tidyr::expand_grid(!!!set_names(xs, toupper))
    #> # A tibble: 4 × 2
    #>       X Y
    #>   <int> <chr>
    #> 1     1 a
    #> 2     1 b
    #> 3     2 a
    #> 4     2 b

### Splicing a list of expressions

Another usage for `!!!` is to inject defused expressions into data-masked dots. However this usage is no longer a common pattern for programming with tidyverse functions and we recommend using other patterns if possible.

First, instead of using the defuse-and-inject pattern with `...`, you can simply pass them on as you normally would. These two expressions are completely equivalent:

    my_group_by <- function(.data, ...) {
      .data %>% dplyr::group_by(!!!enquos(...))
    }

    # This equivalent syntax is preferred
    my_group_by <- function(.data, ...) {
      .data %>% dplyr::group_by(...)
    }

Second, more complex applications such as transformation patterns can be solved with the `across()` operation introduced in dplyr 1.0. Say you want to take the `mean()` of all expressions in `...`. Before `across()`, you had to defuse the `...` expressions, wrap them in a call to `mean()`, and inject them in `summarise()`.

    my_mean <- function(.data, ...) {
      # Defuse dots and auto-name them
      exprs <- enquos(..., .named = TRUE)

      # Wrap the expressions in a call to `mean()`
      exprs <- purrr::map(exprs, ~ call("mean", .x, na.rm = TRUE))

      # Inject them
      .data %>% dplyr::summarise(!!!exprs)
    }

It is much easier to use `across()` instead:

    my_mean <- function(.data, ...) {
      .data %>% dplyr::summarise(across(c(...), ~ mean(.x, na.rm = TRUE)))
    }

### Performance of injected dots and dynamic dots

Take this dynamic dots function:

    n_args <- function(...) {
      length(list2(...))
    }

Because it takes dynamic dots you can splice with `!!!` out of the box.

    n_args(1, 2)
    #> [1] 2

    n_args(!!!mtcars)
    #> [1] 11

Equivalently you could enable `!!!` explicitly with `inject()`.

    inject(n_args(!!!mtcars))
    #> [1] 11

While the result is the same, what is going on under the hood is completely different. `list2()` is a dots collector that special-cases `!!!` arguments. On the other hand, `inject()` operates on the language and creates a function call containing as many arguments as there are elements in the spliced list. If you supply a list of size 1e6, `inject()` is creating one million arguments before evaluation. This can be much slower.

    xs <- rep(list(1), 1e6)

    system.time(
      n_args(!!!xs)
    )
    #>    user  system elapsed
    #>   0.009   0.000   0.009

    system.time(
      inject(n_args(!!!xs))
    )
    #>    user  system elapsed
    #>   0.445   0.012   0.457

The same issue occurs when functions taking dynamic dots are called inside a data-masking function like `dplyr::mutate()`. The mechanism that enables `!!!` injection in these arguments is the same as in `inject()`.

### See Also

- Injecting with !!, !!!, and glue syntax

- `inject()`

- `exec()`

[Package rlang version 1.0.2 Index]