Skip to content
Non-Standard Evaluation
step 1/5

Reading — step 1 of 5

Learn

~2 min readMetaprogramming, Errors, Performance

R has non-standard evaluation (NSE) — functions that capture their arguments unevaluated and operate on the expression itself. This is what makes subset(df, age > 30) work — age > 30 is captured as code, not evaluated in the calling scope.

quote() captures an unevaluated expression:

expr <- quote(x + 1)
expr                # x + 1
class(expr)         # "call"
x <- 10
eval(expr)          # 11
eval(expr, list(x = 99))    # 100

substitute() captures an argument unevaluated:

show <- function(x) {
    e <- substitute(x)
    cat("called with:", deparse(e), "\n")
}

show(1 + 2)         # "called with: 1 + 2"
show(mean(c(1,2)))  # "called with: mean(c(1, 2))"

The expression isn't evaluated — show sees the parse tree.

deparse() turns an expression back into a string:

deparse(quote(x + y * 2))   # "x + y * 2"

bquote() is quote with substitution:

x <- 5
expr <- bquote(.(x) + y)    # 5 + y — x was substituted

Tidy evaluation (rlang, used by tidyverse):

library(rlang)

filter_col <- function(df, col, val) {
    col_quo <- enquo(col)            # capture the expression
    df |> dplyr::filter(!!col_quo == val)
}

filter_col(df, age, 30)              # filters where age == 30

enquo captures the unevaluated argument; !! ("bang-bang") splices it back in. This is how dplyr knows what age means without age being defined in the caller's scope.

Why NSE matters:

  • Domain-specific languages: dplyr, ggplot2, lm (formulas)
  • Better error messages: include the original expression
  • Lazy evaluation: only evaluate if needed

Why it's tricky:

  • Programmatic use is awkward — can't pass col as a variable easily
  • Two solutions: tidy eval (!!, {{ }}) or substitute + eval
  • Debugging is harder — the expression is delayed

Common patterns:

Capture expression for an error message:

assert_that <- function(cond) {
    if (!cond) {
        e <- substitute(cond)
        stop("failed: ", deparse(e))
    }
}
assert_that(1 + 1 == 3)    # Error: failed: 1 + 1 == 3

Build expressions dynamically:

cols <- c("x", "y", "z")
formula_str <- paste("y ~", paste(cols[-1], collapse = " + "))
formula <- as.formula(formula_str)
# y ~ x + z
lm(formula, data = df)

NSE is one of R's most powerful (and confusing) features. Tidy evaluation in rlang is the modern recommended approach.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…