mcpField
R
rlang-patterns

rlang-patterns

github.com/ab604開発21073 回インストール6 週間前に更新
リポジトリ

rlang-patterns について

rlang と tidyverse のデータマスキングを扱う、R パッケージ開発者向けのパターン集です。引数の転送、式の注入、変数の曖昧さを避ける方法を解説します。

主な機能

  1. 01{{}} による引数転送
  2. 02!! と !!! による式・引数の注入
  3. 03動的ドットと名前の注入
  4. 04.data と .env による曖昧さの解消
  5. 05データマスキングと tidy selection の橋渡し

活用シーン

  1. 01dplyr の集計・グループ化関数のラップ
  2. 02データフレーム列へのプログラムによる名前指定アクセス
  3. 03動的引数を受け取る R 関数の作成

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/ab604/claude-code-r-skills --skill rlang-patterns

インストール後、Claude Code で/rlang-patternsを入力して読み込まれたか確認します。

詳細

データマスキングのための現代的な rlang パターン

tidyverse のデータマスキングを支えるメタプログラミングフレームワーク

中核となる概念

データマスキングを使うと、R の式から、データフレームの列を環境内の変数であるかのように参照できます。rlang は、tidyverse のデータマスキングを支えるメタプログラミングフレームワークを提供します。

主な rlang ツール
  • {{}} による抱き込み - 関数の引数をデータマスキング関数に渡します
  • !! による注入 - 単一の式または値を注入します
  • !!! によるスプライス - リストから複数の引数を注入します
  • 動的ドット - 注入をサポートするプログラム可能な ...
  • .data/.env の代名詞 - データと環境の変数を明示的に区別します

関数引数のパターン

{{}} による受け渡し

{{}} を使って、関数の引数をデータマスキング関数に渡します。

# Single argument forwarding
my_summarise <- function(data, var) {
  data |> dplyr::summarise(mean = mean({{ var }}))
}

# Works with any data-masking expression
mtcars |> my_summarise(cyl)
mtcars |> my_summarise(cyl * am)
mtcars |> my_summarise(.data$cyl)  # pronoun syntax supported
... の受け渡し(特別な構文は不要)
# Simple dots forwarding
my_group_by <- function(.data, ...) {
  .data |> dplyr::group_by(...)
}

# Works with tidy selections too
my_select <- function(.data, ...) {
  .data |> dplyr::select(...)
}

# For single-argument tidy selections, wrap in c()
my_pivot_longer <- function(.data, ...) {
  .data |> tidyr::pivot_longer(c(...))
}
.data を使った名前のパターン

プログラムによる列へのアクセスには、.data の代名詞を使います。

# Single column by name
my_mean <- function(data, var) {
  data |> dplyr::summarise(mean = mean(.data[[var]]))
}

# Usage - completely insulated from data-masking
mtcars |> my_mean("cyl")  # No ambiguity, works like regular function

# Multiple columns with all_of()
my_select_vars <- function(data, vars) {
  data |> dplyr::select(all_of(vars))
}

mtcars |> my_select_vars(c("cyl", "am"))

注入演算子

各演算子の使いどころ
演算子用途例
{{ }}関数の引数を渡すsummarise(mean = mean({{ var }}))
!!単一の式/値を注入するsummarise(mean = mean(!!sym(var)))
!!!複数の引数を注入するgroup_by(!!!syms(vars))
.data[[]]名前で列にアクセスするmean(.data[[var]])
!! による高度な注入
# Create symbols from strings
var <- "cyl"
mtcars |> dplyr::summarise(mean = mean(!!sym(var)))

# Inject values to avoid name collisions
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)
mtcars |> dplyr::summarise(mean = mean(!!data_sym(var)))
!!! によるスプライス
# Multiple symbols from character vector
vars <- c("cyl", "am")
mtcars |> dplyr::group_by(!!!syms(vars))

# Or use data_syms() for tidy contexts
mtcars |> dplyr::group_by(!!!data_syms(vars))

# Splice lists of arguments
args <- list(na.rm = TRUE, trim = 0.1)
mtcars |> dplyr::summarise(mean = mean(cyl, !!!args))

動的ドットのパターン

動的ドットをサポートする list2() の使用
my_function <- function(...) {
  # Collect with list2() instead of list() for dynamic features
  dots <- list2(...)
  # Process dots...
}

# Enables these features:
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
グルー構文による名前の注入
# Basic name injection
name <- "result"
list2("{name}" := 1)  # Creates list(result = 1)

# In function arguments with {{
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()
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")

区別のための代名詞

.data と .env のベストプラクティス
# Explicit disambiguation prevents masking issues
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
vars <- c("cyl", "am")
for (var in vars) {
  result <- mtcars |> dplyr::summarise(mean = mean(.data[[var]]))
  print(result)
}

プログラミングのパターン

ブリッジパターン

データマスキングの動作と tidy セレクションの動作を相互に変換します。

# across() as selection-to-data-mask bridge
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
my_group_by <- function(data, vars) {
  data |> dplyr::group_by(across(all_of(vars)))
}

mtcars |> my_group_by(c("cyl", "am"))
変換パターン
# Transform single arguments by wrapping
my_mean <- function(data, var) {
  data |> dplyr::summarise(mean = mean({{ var }}, na.rm = TRUE))
}

# Transform dots with across()
my_means <- function(data, ...) {
  data |> dplyr::summarise(across(c(...), ~ mean(.x, na.rm = TRUE)))
}

# Manual transformation (advanced)
my_means_manual <- function(.data, ...) {
  vars <- enquos(..., .named = TRUE)
  vars <- purrr::map(vars, ~ expr(mean(!!.x, na.rm = TRUE)))
  .data |> dplyr::summarise(!!!vars)
}

避けるべきエラーを招きやすいパターン

非推奨/危険なパターンは使わないでください
# Avoid - String parsing and eval (security risk)
var <- "cyl"
code <- paste("mean(", var, ")")
eval(parse(text = code))  # Dangerous!

# Good - Symbol creation and injection
!!sym(var)  # Safe symbol injection

# Avoid - get() in data mask (name collisions)
with(mtcars, mean(get(var)))  # Collision-prone

# Good - Explicit injection or .data
with(mtcars, mean(!!sym(var)))  # Safe
# or
mtcars |> summarise(mean(.data[[var]]))  # Even safer
よくある間違い
# Don't use {{ }} on non-arguments
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
my_func <- function(data, var) data |> summarise(mean = mean({{ var }}))
# Or: defuse-and-inject pattern
my_func <- function(data, var) {
  var <- enquo(var)
  data |> summarise(mean = mean(!!var))
}

rlang を使ったパッケージ開発

インポート戦略
# In DESCRIPTION:
Imports: rlang

# In NAMESPACE, import specific functions:
importFrom(rlang, enquo, enquos, expr, !!!, :=)

# Or import key functions:
#' @importFrom rlang := enquo enquos
ドキュメントタグ
#' @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
rlang 関数のテスト
# Test data-masking behavior
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
test_that("function supports injection", {
  var <- "cyl"
  result <- my_function(mtcars, !!sym(var))
  expect_true(nrow(result) > 0)
})

この現代的な rlang のアプローチにより、ユーザーが tidyverse 関数に期待する直感的なデータマスキング体験を保ちながら、明快で安全なメタプログラミングを実現できます。

コメント