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).
Generates R code programmatically using rlang's defuse-and-inject pattern.
/plugin marketplace add jsperger/llm-r-skills/plugin install jsperger-r-skills@jsperger/llm-r-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
topic-metaprogramming.mdtopic-multiple-columns.mdtopic-quosure.mdMetaprogramming 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.
| Task | Function/Operator |
|---|---|
| Defuse your own expression | expr(x + 1) |
| Defuse user's single argument | enquo(arg) |
Defuse user's ... arguments | enquos(...) |
| Inject single expression | !! or {{ |
| Splice list of expressions | !!! |
| Get expression from quosure | quo_get_expr(q) |
| Get environment from quosure | quo_get_env(q) |
| Build symbol from string | sym("name") |
| Build symbol with .data pronoun | data_sym("name") |
| Build symbols from vector | syms(names) / data_syms(names) |
| Auto-label expression | as_label(quo) |
| Format argument as string | englue("{{ x }}") |
| Interpolate name in dynamic dots | "{name}" := value |
| Interpolate argument in name | "{{ arg }}" := value |
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
| Function | Defuses | Returns | Use When |
|---|---|---|---|
expr() | Your own code | Expression | Building expressions locally |
enquo() | User's argument | Quosure | Forwarding function arguments |
enquos() | User's ... | List of quosures | Forwarding 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
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"
f(x, y), 1 + 1 - function invocationsx, df - named object references1, "text", NULL - literal valuesA quosure wraps an expression with its original environment. This is critical for correct evaluation when expressions travel across function and package boundaries.
# 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.
| Situation | Use Quosure? |
|---|---|
| Defusing function arguments | Yes - use enquo() |
| Building local expressions | No - use expr() |
| Cross-package composition | Yes - environments matter |
| Simple local evaluation | No - expr() + eval() suffices |
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 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(...))
}
!!! and {name} work in functions using list2()# Enable injection in base functions
inject(
with(mtcars, mean(!!sym("cyl")))
)
Convert strings to symbols:
var <- "cyl"
sym(var)
#> cyl
vars <- c("cyl", "am")
syms(vars)
#> [[1]]
#> cyl
#> [[2]]
#> am
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.
# 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)
In dynamic dots, use glue syntax for names.
{ for Variable Valuesname <- "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 Labelsmy_mutate <- function(data, var) {
data |> dplyr::mutate("mean_{{ var }}" := mean({{ var }}))
}
mtcars |> my_mutate(cyl)
#> Creates column: mean_cyl
my_function <- function(var) {
englue("Column: {{ var }}")
}
my_function(some_column)
#> [1] "Column: some_column"
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)
}
| rlang | Base R | Notes |
|---|---|---|
expr() | bquote() | bquote uses .() for injection |
enquo() | substitute() | substitute returns naked expr, not quosure |
enquos(...) | eval(substitute(alist(...))) | Workaround for dots |
!! | .() in bquote | Only inside bquote |
eval_tidy() | eval() | eval_tidy supports .data/.env pronouns |
{{ 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
Outside tidy eval/inject contexts, operators have different meanings:
| Operator | Intended | Outside Context |
|---|---|---|
{{ | Embrace | Double braces (returns value) |
!! | Inject | Double negation (logical) |
!!! | Splice | Triple negation (logical) |
These fail silently. See the tidy-evaluation skill for details on proper usage contexts.
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")
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Activates when the user asks about Agent Skills, wants to find reusable AI capabilities, needs to install skills, or mentions skills for Claude. Use for discovering, retrieving, and installing skills.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.