metaprogramming

Use when writing R code that manipulates expressions, builds code programmatically, or needs to understand rlang's defuse/inject mechanics. Covers: defusing with expr()/enquo()/enquos(), quosure environment tracking, injection with !!/!!!/{{, symbol construction with sym()/syms(). Does NOT cover: data-mask programming patterns (tidy-evaluation), error handling (rlang-conditions), function design (designing-tidy-r-functions).

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "metaprogramming" with this command: npx skills add jsperger/llm-r-skills/jsperger-llm-r-skills-metaprogramming

R Metaprogramming with rlang

Metaprogramming is the ability to defuse, create, and inject R expressions. The core pattern is defuse-and-inject: capture code as data, optionally transform it, then inject it into another context for evaluation.

Quick Reference

TaskFunction/Operator
Defuse your own expressionexpr(x + 1)
Defuse user's single argumentenquo(arg)
Defuse user's ... argumentsenquos(...)
Inject single expression!! or {{
Splice list of expressions!!!
Get expression from quosurequo_get_expr(q)
Get environment from quosurequo_get_env(q)
Build symbol from stringsym("name")
Build symbol with .data pronoundata_sym("name")
Build symbols from vectorsyms(names) / data_syms(names)
Auto-label expressionas_label(quo)
Format argument as stringenglue("{{ x }}")
Interpolate name in dynamic dots"{name}" := value
Interpolate argument in name"{{ arg }}" := value

Defusing Expressions

Defusing stops evaluation and returns the expression as a tree-like object (a "blueprint" for computation).

# Normal evaluation returns result
1 + 1
#> [1] 2

# Defusing returns the expression
expr(1 + 1)
#> 1 + 1

expr() vs enquo()

FunctionDefusesReturnsUse When
expr()Your own codeExpressionBuilding expressions locally
enquo()User's argumentQuosureForwarding function arguments
enquos()User's ...List of quosuresForwarding multiple arguments
# Defuse your own expression
my_expr <- expr(mean(x, na.rm = TRUE))

# Defuse user's function argument (returns quosure)
my_function <- function(var) {

  enquo(var)
}
my_function(cyl + am)
#> <quosure>
#> expr: ^cyl + am
#> env:  global

enquos() with .named

Auto-label unnamed arguments:

g <- function(...) {

  vars <- enquos(..., .named = TRUE)
  names(vars)
}
g(cyl, 1 + 1)
#> [1] "cyl"   "1 + 1"

g(foo = cyl, bar = 1 + 1)
#> [1] "foo" "bar"

Types of Defused Expressions

  • Calls: f(x, y), 1 + 1 - function invocations
  • Symbols: x, df - named object references
  • Constants: 1, "text", NULL - literal values

Quosures

A quosure wraps an expression with its original environment. This is critical for correct evaluation when expressions travel across function and package boundaries.

Why Environments Matter

# In package A
my_function <- function(data, var) {
  # 'var' was defined in the user's environment
  # The quosure tracks that environment
  var <- enquo(var)

  # When passed to package B's function, the quosure
  # ensures symbols resolve in the correct environment
  pkg_b_function(data, !!var)
}

Without environment tracking, symbols might resolve to wrong objects when code crosses package boundaries.

When You Need Quosures

SituationUse Quosure?
Defusing function argumentsYes - use enquo()
Building local expressionsNo - use expr()
Cross-package compositionYes - environments matter
Simple local evaluationNo - expr() + eval() suffices

Quosure Operations

q <- enquo(x + 1)

quo_get_expr(q)   # Extract expression: x + 1
quo_get_env(q)    # Extract environment

# Create quosure manually
new_quosure(expr(x + 1), env = global_env())

# Convert expression to quosure
as_quosure(expr(x + 1), env = global_env())

Injection Operators

Injection inserts defused expressions back into code before evaluation.

{{ (Embrace)

Defuses and injects in one step. Equivalent to !!enquo(arg):

# These are equivalent:
my_summarise <- function(data, var) {

  data |> dplyr::summarise({{ var }})
}

my_summarise <- function(data, var) {
  data |> dplyr::summarise(!!enquo(var))
}

Use {{ when you simply need to forward an argument. Use enquo() + !! when you need to inspect or transform the expression first.

!! (Bang-Bang)

Injects a single expression:

var <- expr(cyl)
mtcars |> dplyr::summarise(mean(!!var))
#> Equivalent to: summarise(mean(cyl))

# Inject a value to avoid name collisions
x <- 100
df |> dplyr::mutate(x = x / !!x)
#> Uses column x divided by env value 100

!!! (Splice)

Injects each element of a list as separate arguments:

vars <- exprs(cyl, am, vs)
mtcars |> dplyr::select(!!!vars)
#> Equivalent to: select(cyl, am, vs)

# With enquos()
my_group_by <- function(.data, ...) {
  .data |> dplyr::group_by(!!!enquos(...))
}

Where Operators Work

  • Data-masked arguments: Implicitly enabled (dplyr, ggplot2, etc.)
  • inject(): Explicitly enables operators in any context
  • Dynamic dots: !!! and {name} work in functions using list2()
# Enable injection in base functions
inject(
  with(mtcars, mean(!!sym("cyl")))
)

Building Expressions from Data

sym() and syms()

Convert strings to symbols:

var <- "cyl"
sym(var)
#> cyl

vars <- c("cyl", "am")
syms(vars)
#> [[1]]
#> cyl
#> [[2]]
#> am

data_sym() and data_syms()

Create .data$col expressions (safer in tidy eval, avoids collisions):

data_sym("cyl")
#> .data$cyl

data_syms(c("cyl", "am"))
#> [[1]]
#> .data$cyl
#> [[2]]
#> .data$am

Use sym() for base R functions; use data_sym() for tidy eval functions.

Building Calls

# With call()
call("mean", sym("x"), na.rm = TRUE)
#> mean(x, na.rm = TRUE)

# With expr() and injection
var <- sym("x")
expr(mean(!!var, na.rm = TRUE))
#> mean(x, na.rm = TRUE)

Name Interpolation (Glue Operators)

In dynamic dots, use glue syntax for names.

{ for Variable Values

name <- "foo"
tibble::tibble("{name}" := 1:3)
#> # A tibble: 3 x 1
#>     foo
#>   <int>
#> 1     1
#> 2     2
#> 3     3

tibble::tibble("prefix_{name}" := 1:3)
#> Column named: prefix_foo

{{ for Argument Labels

my_mutate <- function(data, var) {
  data |> dplyr::mutate("mean_{{ var }}" := mean({{ var }}))
}
mtcars |> my_mutate(cyl)
#> Creates column: mean_cyl

englue() for String Formatting

my_function <- function(var) {
  englue("Column: {{ var }}")
}
my_function(some_column)
#> [1] "Column: some_column"

Advanced: Manual Expression Transformation

When you need to modify expressions before injection:

my_mean <- function(data, var) {
  # 1. Defuse

  var <- enquo(var)

  # 2. Transform: wrap in mean()
  wrapped <- expr(mean(!!var, na.rm = TRUE))

  # 3. Inject
  data |> dplyr::summarise(mean = !!wrapped)
}

For multiple arguments:

my_mean <- function(.data, ...) {
  vars <- enquos(..., .named = TRUE)

  # Transform each expression
  vars <- purrr::map(vars, ~ expr(mean(!!.x, na.rm = TRUE)))

  .data |> dplyr::summarise(!!!vars)
}

Base R Equivalents

rlangBase RNotes
expr()bquote()bquote uses .() for injection
enquo()substitute()substitute returns naked expr, not quosure
enquos(...)eval(substitute(alist(...)))Workaround for dots
!!.() in bquoteOnly inside bquote
eval_tidy()eval()eval_tidy supports .data/.env pronouns

Pitfalls

{{ on Non-Arguments

{{ should only wrap function arguments. On regular objects, it captures the value, not the expression:

# Correct: var is a function argument
my_fn <- function(var) {{ var }}

# Problematic: x is not an argument
x <- 1
{{ x }}  # Returns 1, not the expression

Operators Out of Context

Outside tidy eval/inject contexts, operators have different meanings:

OperatorIntendedOutside Context
{{EmbraceDouble braces (returns value)
!!InjectDouble negation (logical)
!!!SpliceTriple negation (logical)

These fail silently. See the tidy-evaluation skill for details on proper usage contexts.

See Also

  • tidy-evaluation: Programming patterns for data-masked functions
  • designing-tidy-r-functions: Function API design principles
  • rlang-conditions: Error handling with rlang

Reference Files

Vignettes

Access detailed rlang documentation via R:

# Defusing expressions
vignette("topic-defuse", package = "rlang")

# Injection operators
vignette("topic-inject", package = "rlang")

# Or browse all vignettes
browseVignettes("rlang")

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

ggplot2

No summary provided by upstream source.

Repository SourceNeeds Review
General

hardhat

No summary provided by upstream source.

Repository SourceNeeds Review
General

tidy-evaluation

No summary provided by upstream source.

Repository SourceNeeds Review
General

rlang-conditions

No summary provided by upstream source.

Repository SourceNeeds Review