| name | designing-tidy-r-functions |
| description | Use when designing R function APIs, reviewing R code for design issues, writing functions for R packages, or evaluating argument ordering and naming decisions. Does NOT cover: style/linting, error handling (rlang-conditions), CLI output (r-lib:cli), testing (testing-r-packages), CRAN compliance (cran-extrachecks).
|
Tidy R Function Design
Design R functions for humans, not computers. Optimize for cognitive load reduction, predictability, and composability. These principles apply to any R code, not just tidyverse packages.
Core principle: The less a user needs to think to use your function correctly, the better.
Quick Reference
| Design Goal | Pattern |
|---|
| Predictable names | Verb in imperative mood, prefixes for families |
| Clear arguments | Most important first, optional with defaults last |
| Pipe-friendly | Primary data as first argument |
| Type stability | Output type predictable from input types |
| Enumerated options | Use arg_match() with character vector defaults |
| Side effects | Return input invisibly; partition from computation |
| Complex strategies | Extract to strategy objects (not boolean flags) |
Function Naming
Use Verbs in Imperative Mood
mutate()
filter()
summarize()
geom_point()
recipe()
Prefer Prefixes Over Suffixes
Prefixes enable autocomplete discovery:
str_detect(), str_replace(), str_extract()
read_csv(), read_tsv(), read_delim()
map_int(), map_chr(), map_dbl()
Length Inversely Proportional to Frequency
c(), n(), df
create_bootstrap_samples()
validate_model_specification()
Argument Design
Most Important Arguments First
str_replace(string, pattern, replacement)
left_join(x, y, by)
read_csv(file, col_types, col_names)
Required Arguments Have No Defaults
my_function <- function(data, columns, method = "default") {
}
my_function <- function(data = NULL, columns = NULL, method = "default")
Dots Position Matters
Place ... between required and optional arguments:
my_function <- function(x, y, ..., verbose = FALSE, na.rm = TRUE) {
}
Keep Defaults Short
Use NULL for complex defaults, compute in body:
my_function <- function(x, weights = NULL) {
weights <- weights %||% rep(1, length(x))
}
my_function <- function(x, weights = rep(1, length(x)))
Enumerate String Options
Use arg_match() with character vector defaults:
my_function <- function(x, method = c("fast", "accurate", "balanced")) {
method <- rlang::arg_match(method)
}
Standardize Common Argument Names
| Purpose | Use | Not |
|---|
| New data for prediction | new_data | newdata, newData |
| Missing value handling | na_rm | na.rm, rm.na |
| Case weights | weights | wts, w |
| Predictors (data frame) | x | predictors, features |
| Outcome (data frame) | y | response, target |
| Formula interface data | data | df, dataset |
Output Patterns
Type Stability
Output type should be predictable from input types, not values:
ifelse(TRUE, 1L, 2)
ifelse(FALSE, 1L, 2)
dplyr::if_else(TRUE, 1L, 2L)
dplyr::if_else(FALSE, 1L, 2L)
Tibble Predictions
For modeling functions, predictions should return tibbles:
- Same number of rows as input
- Same row order as input
- Standardized column names:
.pred, .pred_class, .pred_lower
predict(model, new_data)
Side-Effect Functions Return Invisibly
Functions called for side effects should return the first argument invisibly:
write_csv <- function(x, file, ...) {
invisible(x)
}
data |>
write_csv("backup.csv") |>
filter(important) |>
write_csv("filtered.csv")
Side Effects
Partition Side Effects from Computation
analyze <- function(x) {
result <- expensive_computation(x)
cat("Computed result:", result, "\n")
options(my_option = result)
result
}
analyze <- function(x, verbose = FALSE) {
result <- expensive_computation(x)
if (verbose) cli::cli_inform("Computed result: {result}")
result
}
Make Side Effects Easy to Undo
Functions that change global state should return previous values:
old <- options(digits = 3)
options(old)
Strategy Patterns
Avoid Boolean Strategy Flags
grepl(pattern, x, perl = TRUE, fixed = FALSE, ignore.case = TRUE)
str_detect(x, regex(pattern, ignore_case = TRUE))
str_detect(x, fixed(pattern))
Strategy Objects for Complex Options
When strategies need different arguments, create helper functions:
regex <- function(pattern, ignore_case = FALSE, multiline = FALSE) {
structure(list(pattern = pattern, ignore_case = ignore_case,
multiline = multiline), class = "regex")
}
fixed <- function(pattern) {
structure(list(pattern = pattern), class = "fixed")
}
str_detect <- function(string, pattern) {
if (inherits(pattern, "regex")) {
} else if (inherits(pattern, "fixed")) {
}
}
Explicit Over Implicit
Avoid Global Option Dependencies
my_function <- function(x) {
na_action <- getOption("na.action")
}
my_function <- function(x, na_action = na.omit) {
}
Inform Users of Important Defaults
When defaults matter, tell the user:
my_function <- function(x, tz = Sys.timezone()) {
if (missing(tz)) {
cli::cli_inform("Using timezone: {.val {tz}}")
}
}
Model Object Design
Minimize Stored Data
model$training_data <- training_set
model$coefficients <- coefs
model$levels <- factor_levels
Never Save Call Objects
Call objects can embed entire datasets and environments:
model$call <- match.call()
Use Proper S3 Constructors
new_my_model <- function(coefficients, levels) {
structure(
list(coefficients = coefficients, levels = levels),
class = "my_model"
)
}
validate_my_model <- function(x) {
stopifnot(is.numeric(x$coefficients))
x
}
my_model <- function(...) {
result <- new_my_model(...)
validate_my_model(result)
}
Matrix Subsetting Discipline
Always preserve matrix structure:
X[, 1]
X[, 1, drop = FALSE]
Design Review Checklist
When reviewing R function design:
Resources