Quasiquotation

2026-02-19

R sometimes quotes and sometimes doesn’t …

a call to library doesn’t need quotes, but would work just fine with them:

library(ggplot2)

a call to install.packages needs a quoted argument or it fails:

install.packages(ggplot2)
Error:
! object 'ggplot2' not found

Idea of quasiquotation

Quasiquotation is …

the combination of

  • quotation: capturing an expression before evaluation

  • unquotation: selectively evaluate parts of a (quoted/unevaluated) expression.

… useful for programming (really, meta-programming)

Quoting

  • behind a call to library without quotes

  • takes an argument and puts quotes around it

like_paste <- function(..., sep = " ") {
  args <- ensyms(...)
  paste(sapply(args, as_string), collapse = sep)
}

like_paste(I, just, wanted, to, say, hello, "!")
[1] "I just wanted to say hello !"
paste("I", "just", "wanted", "to", "say", "hello", "!")
[1] "I just wanted to say hello !"

Unquoting

When we program, we sometimes don’t want the automatic quotes

time <- "morning"
person <- "Heike"

paste("Good", time, person)
[1] "Good morning Heike"
like_paste(Good, time, person)
[1] "Good time person"

Bang-bang operator !! prevents the automatic quoting:

like_paste(Good, !!time, !!person)
[1] "Good morning Heike"

Partial Unquoting

In the example we call the arguments to paste and like_paste in exactly the opposite way:

paste("Good", time, person)
like_paste(Good, !!time, !!person)

Closer look at Quoting

Four main functions for quoting (i.e. functions that capture an expression/symbol before it’s evaluated)

  • Capturing Expressions:

    • single expression: expr and enexpr
    • multiple expressions: exprs and enexprs
  • Capturing Symbols: sym, ensym, and ensyms (multiple)

expr and enexpr

expr is for interactive use:

e1 <- expr(x) # prevents `x` from being evaluated
e2 <- expr((a + b)/2)

e1; e2
x
(a + b)/2

In a function we are usually more interested in how the function was called, need enriched version of expr:

f1 <- function(x) expr(x) 
f1(a+b/c) # duh
x
f2 <- function(x) enexpr(x) 
f2(a+b/c)
a + b/c

Capturing symbols

ensym and ensyms are special cases of enexpr and enexprs that capture the expression(s) and also check that each expression is a symbol:

f3 <- function(...) ensyms(...)
f3(a, bb = b)
[[1]]
a

$bb
b
f3(a/2, b)
Error in `sym()`:
! Can't convert a call to a symbol.

Your Turn

Can we implement a function my_select now that works the same way as dplyr::select?

my_select <- function (.data, ...) {

}
mtcars |> dplyr::select("mpg", cyl, disp) |> head()
                   mpg cyl disp
Mazda RX4         21.0   6  160
Mazda RX4 Wag     21.0   6  160
Datsun 710        22.8   4  108
Hornet 4 Drive    21.4   6  258
Hornet Sportabout 18.7   8  360
Valiant           18.1   6  225

my_select <- function (.data, ...) {
  vars <- ensyms(...)
#  browser()
  varnames <- sapply(vars, as_string)
  .data[,varnames]
}

mtcars |> my_select("mpg", cyl, disp) |> head()
                   mpg cyl disp
Mazda RX4         21.0   6  160
Mazda RX4 Wag     21.0   6  160
Datsun 710        22.8   4  108
Hornet 4 Drive    21.4   6  258
Hornet Sportabout 18.7   8  360
Valiant           18.1   6  225

Does my_select also work with indices?

mtcars |> dplyr::select(1, 2, 3) |> dim()
[1] 32  3
mtcars |> my_select(1, 2, 3) |> dim()
Error in `sym()`:
! Can't convert a double vector to a symbol.

ensyms allows only symbols

Another approach

my_select <- function (.data, ...) {
  vars <- enexprs(...)
  select_expr <- function(e) {
    if (is_symbol(e) | is.numeric(e)) return(.data[,eval(e)])
      # more complicated case: we have an expression
    browser()
  }
  lapply(vars, FUN = select_expr) |> data.frame()
}

mtcars |> dplyr::select(1, 2, 3) |> dim()
[1] 32  3
mtcars |> my_select(1, 2, 3) |> dim() # dimension is right, names are horribly wrong
[1] 32  3
mtcars |> my_select(1, 2, 3) |> head()
  c.21..21..22.8..21.4..18.7..18.1..14.3..24.4..22.8..19.2..17.8..
1                                                             21.0
2                                                             21.0
3                                                             22.8
4                                                             21.4
5                                                             18.7
6                                                             18.1
  c.6..6..4..6..8..6..8..4..4..6..6..8..8..8..8..8..8..4..4..4..
1                                                              6
2                                                              6
3                                                              4
4                                                              6
5                                                              8
6                                                              6
  c.160..160..108..258..360..225..360..146.7..140.8..167.6..167.6..
1                                                               160
2                                                               160
3                                                               108
4                                                               258
5                                                               360
6                                                               225

We could use the base function subset …?

my_select <- function (.data, ...) {
  vars <- enexprs(...)
  select_expr <- function(e) {
    subset(.data, select = eval(e), subset=T, drop=FALSE)
  }
  lapply(vars, FUN = select_expr) |> data.frame()
}

mtcars |> my_select(1, 2, 3) |> head()
                   mpg cyl disp
Mazda RX4         21.0   6  160
Mazda RX4 Wag     21.0   6  160
Datsun 710        22.8   4  108
Hornet 4 Drive    21.4   6  258
Hornet Sportabout 18.7   8  360
Valiant           18.1   6  225

Using any selector function fails

mtcars |> my_select(everything()) |> head()
Error in `everything()`:
! could not find function "everything"

tidyselect (re-)implements a partial indexing system with :, !, &, | and c()

… we would have the tools though :)

Base R and rlang

  • quote acts like expr

  • alist acts like exprs

  • substitute is most like enexpr

We will stick with rlang for now.

Unquoting operations

  • we have used two functions so far: eval and !!

  • generally, we use eval to evaluate a whole statement

e <- expr(x <- 2*exp(1))

identical(eval(e), x)
[1] TRUE
  • !! is evaluating individual arguments in functions
x <- 2*exp(1)

identical(expr(!!x), x)
[1] TRUE

Your Turn

xy <- expr(x + y)
xz <- expr(x + z)
yz <- expr(y + z)
abc <- exprs(a, b, c)

Use quasiquotation to construct the following calls:

(x + y) / (y + z)
-(x + z) ^ (y + z)
(x + y) + (y + z) - (x + y)
atan2(x + y, y + z)
sum(x + y, x + y, y + z)
sum(a, b, c)
mean(c(a, b, c), na.rm = TRUE)
foo(a = x + y, b = y + z)

Note: !!! evaluates all expressions in a list

expr(!!xy / !!yz)
(x + y)/(y + z)
expr(-(!!xz) ^ (!!yz))
-(x + z)^(y + z)
expr((!!xy) + !!yz - (!!xy))
x + y + (y + z) - (x + y)
expr(atan((!!xy), (!!yz)))
atan(x + y, y + z)
expr(sum(!!!abc))
sum(a, b, c)
expr(mean(c(!!!abc), na.rm=TRUE))
mean(c(a, b, c), na.rm = TRUE)
expr(foo(a = !!xy, b = !!yz))
foo(a = x + y, b = y + z)