metaprogramming-rlang
Tidy evaluation and programmatic tidyverse patterns using rlang. Use this skill when writing functions that accept column names as arguments, building tidyverse-compatible APIs, or working with data-masking and injection operators. Covers embracing with {{}}, injection (!! and !!!), dynamic dots, .data/.env pronouns, name injection with glue syntax, bridge patterns between selection and data-masking, and package development with rlang.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install jeremy-allen-claude-skills-metaprogramming-rlang
Repository
Skill path: metaprogramming-rlang
Tidy evaluation and programmatic tidyverse patterns using rlang. Use this skill when writing functions that accept column names as arguments, building tidyverse-compatible APIs, or working with data-masking and injection operators. Covers embracing with {{}}, injection (!! and !!!), dynamic dots, .data/.env pronouns, name injection with glue syntax, bridge patterns between selection and data-masking, and package development with rlang.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Data / AI, Tech Writer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: jeremy-allen.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install metaprogramming-rlang into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/jeremy-allen/claude-skills before adding metaprogramming-rlang to shared team environments
- Use metaprogramming-rlang for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: metaprogramming-rlang
description: |
Tidy evaluation and programmatic tidyverse patterns using rlang. Use this skill when writing functions that accept column names as arguments, building tidyverse-compatible APIs, or working with data-masking and injection operators. Covers embracing with {{}}, injection (!! and !!!), dynamic dots, .data/.env pronouns, name injection with glue syntax, bridge patterns between selection and data-masking, and package development with rlang.
---
# Metaprogramming rlang
This skill covers modern rlang patterns for data-masking, tidy evaluation, and building programmatic tidyverse functions.
## Core Concepts
**Data-masking** allows R expressions to refer to data frame columns as if they were variables in the environment. rlang provides the metaprogramming framework that powers tidyverse data-masking.
### Key rlang Tools
- **Embracing `{{}}`** - Forward function arguments to data-masking functions
- **Injection `!!`** - Inject single expressions or values
- **Splicing `!!!`** - Inject multiple arguments from a list
- **Dynamic dots** - Programmable `...` with injection support
- **Pronouns `.data`/`.env`** - Explicit disambiguation between data and environment variables
## When to Use Each Operator
| Operator | Use Case | Example |
|----------|----------|---------|
| `{{ }}` | Forward function arguments | `summarise(mean = mean({{ var }}))` |
| `!!` | Inject single expression/value | `summarise(mean = mean(!!sym(var)))` |
| `!!!` | Inject multiple arguments | `group_by(!!!syms(vars))` |
| `.data[[]]` | Access columns by name | `mean(.data[[var]])` |
## Function Argument Patterns
### Forwarding with `{{}}`
Use `{{}}` to forward function arguments to data-masking functions. See [embrace-examples.md](references/embrace-examples.md).
### Forwarding `...`
No special syntax needed for dots forwarding. See [dots-forwarding.md](references/dots-forwarding.md).
### Names Patterns with `.data`
Use `.data` pronoun for programmatic column access. See [data-pronoun-examples.md](references/data-pronoun-examples.md).
## Injection Operators
### Advanced Injection with `!!`
Create symbols from strings, inject values to avoid name collisions. See [injection-examples.md](references/injection-examples.md).
### Splicing with `!!!`
Inject multiple symbols from character vectors, splice lists of arguments. See [splicing-examples.md](references/splicing-examples.md).
## Dynamic Dots Patterns
### Using `list2()` for Dynamic Dots Support
Enables splicing, name injection, and trailing commas. See [dynamic-dots-examples.md](references/dynamic-dots-examples.md).
### Name Injection with Glue Syntax
Use glue syntax for dynamic column naming. See [name-injection-examples.md](references/name-injection-examples.md).
## Pronouns for Disambiguation
### `.data` and `.env` Best Practices
Explicit disambiguation prevents masking issues. See [pronouns-examples.md](references/pronouns-examples.md).
## Programming Patterns
### Bridge Patterns
Converting between data-masking and tidy selection behaviors:
- `across()` as selection-to-data-mask bridge
- `across(all_of())` as names-to-data-mask bridge
See [bridge-patterns.md](references/bridge-patterns.md).
### Transformation Patterns
Transform single arguments by wrapping, transform dots with `across()`. See [transformation-patterns.md](references/transformation-patterns.md).
## Error-Prone Patterns to Avoid
### Deprecated/Dangerous Patterns
- String parsing with `eval(parse(text = ...))` - Security risk
- `get()` in data mask - Name collision prone
See [avoid-patterns.md](references/avoid-patterns.md).
### Common Mistakes
- Don't use `{{ }}` on non-arguments
- Don't mix injection styles unnecessarily
## Package Development with rlang
### Import Strategy
```r
# In DESCRIPTION:
Imports: rlang
# In NAMESPACE, import specific functions:
importFrom(rlang, enquo, enquos, expr, !!!, :=)
```
### Documentation Tags
```r
#' @param var <[`data-masked`][dplyr::dplyr_data_masking]> Column to summarize
#' @param ... <[`dynamic-dots`][rlang::dyn-dots]> Additional grouping variables
#' @param cols <[`tidy-select`][dplyr::dplyr_tidy_select]> Columns to select
```
### Testing rlang Functions
See [testing-examples.md](references/testing-examples.md) for testing data-masking and injection behavior.
source: Sarah Johnson's gist https://gist.github.com/sj-io/3828d64d0969f2a0f05297e59e6c15ad
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/embrace-examples.md
```markdown
# Forwarding with {{}} (Embrace)
Use {{}} to forward function arguments to data-masking functions.
### Single argument forwarding
```r
my_summarise <- function(data, var) {
data |> dplyr::summarise(mean = mean({{ var }}))
}
```
### Works with any data-masking expression
```r
mtcars |> my_summarise(cyl)
mtcars |> my_summarise(cyl * am)
mtcars |> my_summarise(.data$cyl) # pronoun syntax supported
```
```
### references/dots-forwarding.md
```markdown
# Forwarding ... (No Special Syntax Needed)
### Simple dots forwarding
```r
my_group_by <- function(.data, ...) {
.data |> dplyr::group_by(...)
}
```
### Works with tidy selections too
```r
my_select <- function(.data, ...) {
.data |> dplyr::select(...)
}
```
### For single-argument tidy selections, wrap in c()
```r
my_pivot_longer <- function(.data, ...) {
.data |> tidyr::pivot_longer(c(...))
}
```
```
### references/data-pronoun-examples.md
```markdown
# Names Patterns with .data
Use .data pronoun for programmatic column access.
### Single column by name
```r
my_mean <- function(data, var) {
data |> dplyr::summarise(mean = mean(.data[[var]]))
}
```
### Usage - completely insulated from data-masking
```r
mtcars |> my_mean("cyl") # No ambiguity, works like regular function
```
### Multiple columns with all_of()
```r
my_select_vars <- function(data, vars) {
data |> dplyr::select(all_of(vars))
}
mtcars |> my_select_vars(c("cyl", "am"))
```
```
### references/injection-examples.md
```markdown
# Advanced Injection with !!
### Create symbols from strings
```r
var <- "cyl"
mtcars |> dplyr::summarise(mean = mean(!!sym(var)))
```
### Inject values to avoid name collisions
```r
df <- data.frame(x = 1:3)
x <- 100
df |> dplyr::mutate(scaled = x / !!x) # Uses both data and env x
```
### Use data_sym() for tidyeval contexts (more robust)
```r
mtcars |> dplyr::summarise(mean = mean(!!data_sym(var)))
```
```
### references/splicing-examples.md
```markdown
# Splicing with !!!
### Multiple symbols from character vector
```r
vars <- c("cyl", "am")
mtcars |> dplyr::group_by(!!!syms(vars))
```
### Or use data_syms() for tidy contexts
```r
mtcars |> dplyr::group_by(!!!data_syms(vars))
```
### Splice lists of arguments
```r
args <- list(na.rm = TRUE, trim = 0.1)
mtcars |> dplyr::summarise(mean = mean(cyl, !!!args))
```
```
### references/dynamic-dots-examples.md
```markdown
# Using list2() for Dynamic Dots Support
```r
my_function <- function(...) {
# Collect with list2() instead of list() for dynamic features
dots <- list2(...)
# Process dots...
}
```
## Enables these features
```r
my_function(a = 1, b = 2) # Normal usage
my_function(!!!list(a = 1, b = 2)) # Splice a list
my_function("{name}" := value) # Name injection
my_function(a = 1, ) # Trailing commas OK
```
```
### references/name-injection-examples.md
```markdown
# Name Injection with Glue Syntax
### Basic name injection
```r
name <- "result"
list2("{name}" := 1) # Creates list(result = 1)
```
### In function arguments with {{
```r
my_mean <- function(data, var) {
data |> dplyr::summarise("mean_{{ var }}" := mean({{ var }}))
}
mtcars |> my_mean(cyl) # Creates column "mean_cyl"
mtcars |> my_mean(cyl * am) # Creates column "mean_cyl * am"
```
### Allow custom names with englue()
```r
my_mean <- function(data, var, name = englue("mean_{{ var }}")) {
data |> dplyr::summarise("{name}" := mean({{ var }}))
}
# User can override default
mtcars |> my_mean(cyl, name = "cylinder_mean")
```
```
### references/pronouns-examples.md
```markdown
# .data and .env Best Practices
Explicit disambiguation prevents masking issues.
```r
cyl <- 1000 # Environment variable
mtcars |> dplyr::summarise(
data_cyl = mean(.data$cyl), # Data frame column
env_cyl = mean(.env$cyl), # Environment variable
ambiguous = mean(cyl) # Could be either (usually data wins)
)
```
### Use in loops and programmatic contexts
```r
vars <- c("cyl", "am")
for (var in vars) {
result <- mtcars |> dplyr::summarise(mean = mean(.data[[var]]))
print(result)
}
```
```
### references/bridge-patterns.md
```markdown
# Bridge Patterns
Converting between data-masking and tidy selection behaviors.
### across() as selection-to-data-mask bridge
```r
my_group_by <- function(data, vars) {
data |> dplyr::group_by(across({{ vars }}))
}
# Works with tidy selection
mtcars |> my_group_by(starts_with("c"))
```
### across(all_of()) as names-to-data-mask bridge
```r
my_group_by <- function(data, vars) {
data |> dplyr::group_by(across(all_of(vars)))
}
mtcars |> my_group_by(c("cyl", "am"))
```
```
### references/transformation-patterns.md
```markdown
# Transformation Patterns
### Transform single arguments by wrapping
```r
my_mean <- function(data, var) {
data |> dplyr::summarise(mean = mean({{ var }}, na.rm = TRUE))
}
```
### Transform dots with across()
```r
my_means <- function(data, ...) {
data |> dplyr::summarise(across(c(...), ~ mean(.x, na.rm = TRUE)))
}
```
### Manual transformation (advanced)
```r
my_means_manual <- function(.data, ...) {
vars <- enquos(..., .named = TRUE)
vars <- purrr::map(vars, ~ expr(mean(!!.x, na.rm = TRUE)))
.data |> dplyr::summarise(!!!vars)
}
```
```
### references/avoid-patterns.md
```markdown
# Error-Prone Patterns to Avoid
## Deprecated/Dangerous Patterns
### Avoid - String parsing and eval (security risk)
```r
var <- "cyl"
code <- paste("mean(", var, ")")
eval(parse(text = code)) # Dangerous!
```
### Good - Symbol creation and injection
```r
!!sym(var) # Safe symbol injection
```
### Avoid - get() in data mask (name collisions)
```r
with(mtcars, mean(get(var))) # Collision-prone
```
### Good - Explicit injection or .data
```r
with(mtcars, mean(!!sym(var))) # Safe
# or
mtcars |> summarise(mean(.data[[var]])) # Even safer
```
## Common Mistakes
### Don't use {{ }} on non-arguments
```r
my_func <- function(x) {
x <- force(x) # x is now a value, not an argument
quo(mean({{ x }})) # Wrong! Captures value, not expression
}
```
### Don't mix injection styles unnecessarily
Pick one approach and stick with it:
### Either: embrace pattern
```r
my_func <- function(data, var) data |> summarise(mean = mean({{ var }}))
```
### Or: defuse-and-inject pattern
```r
my_func <- function(data, var) {
var <- enquo(var)
data |> summarise(mean = mean(!!var))
}
```
```
### references/testing-examples.md
```markdown
# Testing rlang Functions
### Test data-masking behavior
```r
test_that("function supports data masking", {
result <- my_function(mtcars, cyl)
expect_equal(names(result), "mean_cyl")
# Test with expressions
result2 <- my_function(mtcars, cyl * 2)
expect_true("mean_cyl * 2" %in% names(result2))
})
```
### Test injection behavior
```r
test_that("function supports injection", {
var <- "cyl"
result <- my_function(mtcars, !!sym(var))
expect_true(nrow(result) > 0)
})
```
```