When a piece of R code is defused, R doesn't return its value like it normally would. Instead it returns the expression in a special tree-like object that describes how to compute a value. These defused expressions can be thought of as blueprints or recipes for computing values.

Using [expr()] we can observe the difference between computing an expression and defusing it:

# Return the result of `1 + 1`
1 + 1

# Return the expression `1 + 1`
expr(1 + 1)

Evaluation of a defused expression can be resumed at any time with [eval()] (see also [eval_tidy()]).

# Return the expression `1 + 1`
e <- expr(1 + 1)

# Return the result of `1 + 1`
eval(e)

The most common use case for defusing expressions is to resume its evaluation in a [data mask][topic-data-mask]. This makes it possible for the expression to refer to columns of a data frame as if they were regular objects.

e <- expr(mean(cyl))
eval(e, mtcars)

Do I need to know about defused expressions?

As a tidyverse user you will rarely need to defuse expressions manually with expr(), and even more rarely need to resume evaluation with [eval()] or [eval_tidy()]. Instead, you call [data-masking][topic-data-mask] functions which take care of defusing your arguments and resuming them in the context of a data mask.

mtcars %>% dplyr::summarise(
  mean(cyl)  # This is defused and data-masked
)

It is important to know that a function defuses its arguments because it requires slightly different methods when called from a function. The main thing is that arguments must be transported with the [embrace operator][embrace-operator] {{. It allows the data-masking function to defuse the correct expression.

my_mean <- function(data, var) {
  dplyr::summarise(data, mean = mean({{ var }}))
}

Read more about this in:

The booby trap analogy

The term "defusing" comes from an analogy to the evaluation model in R. As you may know, R uses lazy evaluation, which means that arguments are only evaluated when they are needed for a computation. Let's take two functions, ignore() which doesn't do anything with its argument, and force() which returns it:

ignore <- function(arg) NULL
force <- function(arg) arg

ignore(warning("boom"))

force(warning("boom"))

A warning is only emitted when the function actually triggers evaluation of its argument. Evaluation of arguments can be chained by passing them to other functions. If one of the functions ignores its argument, it breaks the chain of evaluation.

f <- function(x) g(x)
g <- function(y) h(y)
h <- function(z) ignore(z)

f(warning("boom"))

In a way, arguments are like booby traps which explode (evaluate) when touched. Defusing an argument can be seen as defusing the booby trap.

expr(force(warning("boom")))

Types of defused expressions

You can create new call or symbol objects by using the defusing function expr():

# Create a symbol representing objects called `foo`
expr(foo)
#> foo

# Create a call representing the computation of the mean of `foo`
expr(mean(foo, na.rm = TRUE))
#> mean(foo, na.rm = TRUE)

# Return a constant
expr(1)
#> [1] 1

expr(NULL)
#> NULL

Defusing is not the only way to create defused expressions. You can also assemble them from data:

# Assemble a symbol from a string
var <- "foo"
sym(var)

# Assemble a call from strings, symbols, and constants
call("mean", sym(var), na.rm = TRUE)

Local expressions versus function arguments

There are two main ways to defuse expressions, to which correspond two functions in rlang, [expr()] and [enquo()]:

Defuse and inject

One purpose for defusing evaluation of an expression is to interface with [data-masking][topic-data-mask] functions by injecting the expression back into another function with !!. This is the [defuse-and-inject pattern][topic-metaprogramming].

my_summarise <- function(data, arg) {
  # Defuse the user expression in `arg`
  arg <- enquo(arg)

  # Inject the expression contained in `arg`
  # inside a `summarise()` argument
  data |> dplyr::summarise(mean = mean(!!arg, na.rm = TRUE))
}

Defuse-and-inject is usually performed in a single step with the embrace operator r link("{{").

my_summarise <- function(data, arg) {
  # Defuse and inject in a single step with the embracing operator
  data |> dplyr::summarise(mean = mean({{ arg }}, na.rm = TRUE))
}

Using enquo() and !! separately is useful in more complex cases where you need access to the defused expression instead of just passing it on.

Defused arguments and quosures

If you inspect the return values of expr() and enquo(), you'll notice that the latter doesn't return a raw expression like the former. Instead it returns a [quosure], a wrapper containing an expression and an environment.

expr(1 + 1)
#> 1 + 1

my_function <- function(arg) enquo(arg)
my_function(1 + 1)
#> <quosure>
#> expr: ^1 + 1
#> env:  global

R needs information about the environment to properly evaluate argument expressions because they come from a different context than the current function. For instance when a function in your package calls dplyr::mutate(), the quosure environment indicates where all the private functions of your package are defined.

Read more about the role of quosures in r link("topic_quosure").

Comparison with base R

Defusing is known as quoting in other frameworks.



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