17.2 Code is data

The first big idea is that code is data: you can capture code and compute on as you can with any other type of data. The first way you can capture code is with rlang::expr(). You can think of expr() as returning exactly what you pass in:

expr(mean(x, na.rm = TRUE))
#> mean(x, na.rm = TRUE)
expr(10 + 100 + 1000)
#> 10 + 100 + 1000

More formally, captured code is called an expression. An expression isn’t a single type of object, but is a collective term for any of four types (call, symbol, constant, or pairlist), which you’ll learn more about in Chapter 18.

expr() lets you capture code that you’ve typed. You need a different tool to capture code passed to a function because expr() doesn’t work:

capture_it <- function(x) {
  expr(x)
}
capture_it(a + b + c)
#> x

Here you need to use a function specifically designed to capture user input in a function argument: enexpr(). Think of the “en” in the context of “enrich”: enexpr() takes a lazily evaluated argument and turns it into an expression:

capture_it <- function(x) {
  enexpr(x)
}
capture_it(a + b + c)
#> a + b + c

Because capture_it() uses enexpr() we say that it automatically quotes its first argument. You’ll learn more about this term in Section 19.2.1.

Once you have captured an expression, you can inspect and modify it. Complex expressions behave much like lists. That means you can modify them using [[ and $:

f <- expr(f(x = 1, y = 2))

# Add a new argument
f$z <- 3
f
#> f(x = 1, y = 2, z = 3)

# Or remove an argument:
f[[2]] <- NULL
f
#> f(y = 2, z = 3)

The first element of the call is the function to be called, which means the first argument is in the second position. You’ll learn the full details in Section 18.3.