Programming with Tidy Evaluation in R

· 4 min read · Updated March 18, 2026 · intermediate
r tidyverse dplyr rlang programming

Tidy evaluation is a powerful paradigm in R that allows you to write functions and code that work naturally with the tidyverse. If you’ve used dplyr, ggplot2, or other tidyverse packages, you’ve already benefited from tidy evaluation—even if you didn’t realize it. Understanding how to program with tidy evaluation opens up the ability to create reusable data manipulation functions that feel as natural as writing dplyr pipelines directly.

What is Tidy Evaluation?

Standard evaluation in R works straightforwardly: you pass an object, and R evaluates it. But the tidyverse uses non-standard evaluation (NSE), where functions capture expressions rather than just their values. This is what allows you to write filter(df, x > 5) instead of filter(df, df$x > 5).

The problem arises when you want to program with these functions. If you try to pass a variable containing a column name:

my_col <- "mpg"
filter(mtcars, my_col > 20)

R interprets my_col as a literal variable, not as the column mpg. This is where tidy evaluation techniques come in.

Tidy evaluation provides mechanisms to capture user input as expressions and then evaluate them in the right context. The key concepts are quasiquotation and quosures, which work together to make programming with tidyverse functions possible.

Quasiquotation: The !! Operator

The bang-bang operator (!!) is the primary tool for injecting values into captured expressions. Think of it as “unquote” — it forces early evaluation of part of an expression before the entire expression is evaluated.

Here’s a simple example:

library(dplyr)

col_name <- sym("mpg")
mtcars %>% filter(!!col_name > 20)

The sym() function creates a symbol from a string, and !! injects that symbol into the expression. The expression becomes filter(mtcars, mpg > 20) before evaluation.

In practice, you’ll more often use enquo() to capture user-provided arguments:

filter_by_column <- function(df, col, threshold) {
  col_expr <- enquo(col)
  df %>% filter(!!col_expr > threshold)
}

mtcars %>% filter_by_column(mpg, 20)

The enquo() function captures the expression supplied by the user and wraps it in a quosure, which preserves both the expression and its environment.

The !!! Operator for List Injection

Sometimes you have multiple arguments stored in a list and want to inject them all at once. The splat operator (!!!) does exactly this — it unpacks a list of expressions into a function call.

args <- list(
  sym("mpg"),
  sym("cyl")
)

mtcars %>% select(!!!args)
# Equivalent to: select(mtcars, mpg, cyl)

This is particularly useful when building up arguments dynamically or when working with !!! inside purrr::inject().

The !!_ Operator for Big Data

The !!_ operator (pronounced “bang-bang-under”) is a variant that works specifically with rlang’s UQE (unquote evaluation) pattern. It’s useful when working with large datasets or when you need more explicit control over how expressions are unquoted. In most everyday programming scenarios, !! is sufficient.

The {{ }} Syntax for Column Injection

The curly brace syntax {{ }} (sometimes called “curly-curly”) provides a more readable way to inject column names. Introduced in rlang 0.4.0, it works directly with bare column names:

group_and_summarize <- function(df, group_col, sum_col) {
  df %>%
    group_by({{ group_col }}) %>%
    summarise(avg = mean({{ sum_col }}), .groups = "drop")
}

mtcars %>% group_and_summarize(cyl, mpg)

The {{ }} operator is syntactic sugar that handles the enquo() and !! pattern automatically. It captures the user’s input and injects it into the expression. This makes your function interfaces cleaner and more intuitive for users.

Practical Examples with dplyr

Let’s put these concepts together to build a reusable summary function:

library(dplyr)
library(rlang)

summarise_group <- function(data, group_var, summary_var) {
  group_expr <- enquo(group_var)
  summary_expr <- enquo(summary_var)
  
  data %>%
    group_by(!!group_expr) %>%
    summarise(
      n = n(),
      mean = mean(!!summary_expr, na.rm = TRUE),
      sd = sd(!!summary_expr, na.rm = TRUE),
      .groups = "drop"
    )
}

mtcars %>% summarise_group(cyl, mpg)

Output:

# A tibble: 3 × 4
    cyl     n  mean    sd
  <dbl> <int> <dbl> <dbl>
1     4    11  26.7  4.51
2     6     7  19.7  2.01
3     8    14  15.1  3.70

Here’s another example using the {{ }} syntax with ggplot2:

plot_by_group <- function(data, x_var, y_var, colour_var) {
  ggplot(data, aes(x = {{ x_var }}, y = {{ y_var }}, colour = {{ colour_var }})) +
    geom_point() +
    labs(x = rlang::as_label(enquo(x_var)),
         y = rlang::as_label(enquo(y_var)),
         colour = rlang::as_label(enquo(colour_var)))
}

mtcars %>% plot_by_group(wt, mpg, cyl)

When to Use Each Approach

OperatorUse Case
!!Inject a single symbol or expression
!!!Inject multiple arguments from a list
{{ }}Clean syntax for column injection in function arguments

The {{ }} syntax is now the recommended approach for most dplyr programming tasks because it reduces boilerplate and makes your intent clearer. However, understanding !! and !!! helps you debug issues and work with more advanced patterns.

See Also

For more information on specific tidyverse functions that use tidy evaluation:

Explore the tidyverse reference pages for more examples of how these functions work together.