topic-defuse: Defusing R expressions

topic-defuseR Documentation

Defusing R expressions

Description

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
#> [1] 2

# Return the expression `1 + 1`
expr(1 + 1)
#> 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)
#> [1] 2

The most common use case for defusing expressions is to resume its evaluation in a 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)
#> [1] 6.1875

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 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
)
#> # A tibble: 1 x 1
#>   `mean(cyl)`
#>         <dbl>
#> 1        6.19

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 ⁠{{⁠. 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:

  • What is data-masking and why do I need {{?

  • Data mask programming patterns

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"))
#> NULL

force(warning("boom"))
#> Warning in force(warning("boom")): 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"))
#> NULL

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")))
#> force(warning("boom"))

Types of defused expressions

  • Calls, like f(1, 2, 3) or 1 + 1 represent the action of calling a function to compute a new value, such as a vector.

  • Symbols, like x or df, represent named objects. When the object pointed to by the symbol was defined in a function or in the global environment, we call it an environment-variable. When the object is a column in a data frame, we call it a data-variable.

  • Constants, like 1 or NULL.

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

  • You can defuse your own R expressions with expr().

  • You can defuse the expressions supplied by the user of your function with the en-prefixed operators, such as enquo() and enquos(). These operators defuse function arguments.

Defuse and inject

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

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 {{.

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 What are quosures and when are they needed?.

Comparison with base R

Defusing is known as quoting in other frameworks.

  • The equivalent of expr() is base::bquote().

  • The equivalent of enquo() is base::substitute(). The latter returns a naked expression instead of a quosure.

  • There is no equivalent for enquos(...) but you can defuse dots as a list of naked expressions with eval(substitute(alist(...))).


hadley/rlang documentation built on Dec. 17, 2024, 3:26 a.m.