- name
- R Code Patterns for Packages
- description
- Critical rules and patterns for writing R code in packages, including forbidden functions, namespace management, and package state handling
# R Code Patterns for Packages
## Overview
Writing code for R packages differs significantly from writing R scripts. This skill covers the critical rules, forbidden functions, and best practices that prevent common package development errors.
## CRITICAL: Forbidden Functions in R/
These functions should **NEVER** appear in your package's R/ code:
### NEVER Use library() or require()
**Why**: These modify the user's search path and create dependencies on load order.
```r
# WRONG - DO NOT DO THIS:
my_function <- function(x) {
library(dplyr)
x %>% filter(value > 0)
}
# RIGHT - Use explicit namespace:
my_function <- function(x) {
dplyr::filter(x, value > 0)
}
# ALSO RIGHT - Import in NAMESPACE via roxygen2:
#' @importFrom dplyr filter
my_function <- function(x) {
filter(x, value > 0)
}
```
**Exception**: `require()` is acceptable for Suggests packages, but use `requireNamespace()` instead:
```r
# ACCEPTABLE but not ideal:
if (require("ggplot2")) {
# code using ggplot2
}
# BETTER:
if (requireNamespace("ggplot2", quietly = TRUE)) {
# code using ggplot2
}
# BEST - with helpful error message:
check_suggested <- function(pkg) {
if (!requireNamespace(pkg, quietly = TRUE)) {
stop(
"Package '", pkg, "' required but not installed.\n",
"Install with: install.packages('", pkg, "')",
call. = FALSE
)
}
}
```
### NEVER Use source()
**Why**: Package code is loaded via the namespace mechanism, not sourcing.
```r
# WRONG:
source("R/utils.R")
# RIGHT:
# Just define functions in R/ files. They're automatically loaded.
# If you need execution order, use alphabetical naming or .onLoad()
```
### NEVER Use ::: (Triple Colon)
**Why**: Accessing internal functions from other packages is fragile and violates encapsulation.
```r
# WRONG:
result <- dplyr:::some_internal_function(x)
# RIGHT:
# Either:
# 1. The other package should export it
# 2. Copy the code (with attribution) if license permits
# 3. Use a different approach
# 4. Ask the maintainer to export it
```
**Exception**: Acceptable in tests to test your own internal functions:
```r
# In tests/testthat/test-internal.R:
test_that("internal function works", {
expect_equal(mypackage:::.internal_function(5), 10)
})
```
## Namespace Management
### Default Pattern: pkg::fun()
For packages listed in `Imports:`, use explicit namespace references:
```r
#' @param x A data frame
my_function <- function(x) {
# Explicit namespace - always works
dplyr::mutate(x, new_col = value * 2)
}
```
**Advantages:**
- Clear where functions come from
- No NAMESPACE imports needed for simple usage
- Easier to understand code provenance
### Import Pattern: @importFrom
For heavily-used functions, import to your namespace:
```r
#' @importFrom dplyr filter mutate select
#' @importFrom rlang .data
my_function <- function(x) {
x %>%
filter(.data$value > 0) %>%
mutate(doubled = .data$value * 2) %>%
select(.data$id, .data$doubled)
}
```
**When to import:**
- Functions used in many places (reduces typing)
- Infix operators (%>%, %||%, :=)
- Functions from your package's core dependencies
- Performance-critical code (tiny speedup from avoiding :::)
### Full Import: @import (Rare)
Import an entire package namespace (almost never recommended):
```r
#' @import rlang
```
**Only use for:**
- rlang (if building a tidyverse-style package)
- Your own package's internal utilities package
**Why avoid:**
- Imports ALL exported functions (namespace pollution)
- Risk of function name conflicts
- Makes code provenance unclear
- Can break if imported package adds conflicting exports
## R Code Restrictions
### Use TRUE/FALSE, Never T/F
**Why**: T and F are variables that can be reassigned; TRUE and FALSE are reserved words.
```r
# WRONG:
if (x == T) {
return(F)
}
# RIGHT:
if (x == TRUE) {
return(FALSE)
}
# Why it matters:
T <- FALSE # User can do this!
x == T # Now broken
```
### ASCII-Only in .R Files
**Why**: R CMD check enforces ASCII in package R code for portability.
```r
# WRONG:
message("Processing data → results")
city <- "São Paulo"
# RIGHT:
message("Processing data -> results") # ASCII arrow
city <- "S\u00e3o Paulo" # Unicode escape
# For documentation (roxygen2 comments), UTF-8 is fine:
#' @param city City name (e.g., "São Paulo")
```
**Handling Unicode:**
```r
# Use Unicode escapes:
"\u00e9" # é
"\u2192" # →
"\u2264" # ≤
# Or use iconv():
iconv("São Paulo", to = "ASCII//TRANSLIT")
```
### Global State and Options
**CRITICAL**: Never modify global state without restoration.
```r
# WRONG - leaves user's session modified:
my_function <- function() {
options(stringsAsFactors = FALSE)
# ... code ...
}
# RIGHT - restore on exit:
my_function <- function() {
old_opts <- options(stringsAsFactors = FALSE)
on.exit(options(old_opts), add = TRUE)
# ... code ...
}
# BETTER - use withr:
my_function <- function() {
withr::local_options(stringsAsFactors = FALSE)
# ... code ...
}
```
**Common global state to be careful with:**
- `options()`
- `par()` (graphics parameters)
- Working directory (`setwd()`)
- Random seed (`set.seed()`)
- Environment variables
```r
# Always restore:
plot_something <- function() {
old_par <- par(mfrow = c(2, 2))
on.exit(par(old_par), add = TRUE)
# ... plotting code ...
}
# Or use withr:
plot_something <- function() {
withr::local_par(mfrow = c(2, 2))
# ... plotting code ...
}
```
## Build-Time vs Load-Time Code
### Understanding Execution Timing
**CRITICAL**: Top-level code in R/ files executes during `R CMD build`, not when users load your package!
```r
# This runs at BUILD time (on your machine):
message("Building package") # Users never see this!
current_year <- as.integer(format(Sys.Date(), "%Y")) # Frozen at build time!
# This runs at LOAD time (on user's machine):
.onLoad <- function(libname, pkgname) {
message("Loading package") # Users see this
}
my_function <- function() {
year <- as.integer(format(Sys.Date(), "%Y")) # Computed each call
# ...
}
```
### What Belongs Where
```r
# TOP-LEVEL (Build time):
# - Function definitions (always)
# - Package constants
# - Nothing that queries the system!
# Examples:
PACKAGE_VERSION <- "1.0.0" # OK - constant
API_ENDPOINT <- "https://api.example.com" # OK - constant
# WRONG - these run at build time:
USER_HOME <- Sys.getenv("HOME") # Gets YOUR home, not user's!
N_CORES <- parallel::detectCores() # Gets YOUR cores!
IS_WINDOWS <- .Platform$OS.type == "windows" # Gets YOUR OS!
# RIGHT - query at runtime:
get_user_home <- function() {
Sys.getenv("HOME")
}
get_n_cores <- function() {
parallel::detectCores()
}
is_windows <- function() {
.Platform$OS.type == "windows"
}
```
## Package State Management
### Using Environments for State
Packages should not use global variables. Use environments instead:
```r
# Create package environment (at build time):
pkg_env <- new.env(parent = emptyenv())
# Initialize state (at load time):
.onLoad <- function(libname, pkgname) {
pkg_env$cache <- list()
pkg_env$config <- list(
api_key = NULL,
verbose = FALSE
)
}
# Access in functions:
get_config <- function(key) {
pkg_env$config[[key]]
}
set_config <- function(key, value) {
pkg_env$config[[key]] <- value
invisible(value)
}
cache_get <- function(key) {
pkg_env$cache[[key]]
}
cache_set <- function(key, value) {
pkg_env$cache[[key]] <- value
invisible(value)
}
```
**Why `parent = emptyenv()`:**
- Prevents accidental variable lookup in global environment
- Keeps package environment isolated
- Makes dependencies explicit
### .onLoad() and .onAttach()
Place these in `R/zzz.R` (alphabetically last, so other code is already sourced):
```r
# R/zzz.R
.onLoad <- function(libname, pkgname) {
# Run when namespace is loaded
# Setup package state, register S3 methods, etc.
# NO messages to user here!
# Initialize package environment:
pkg_env$cache <- new.env(parent = emptyenv())
# Register S3 methods dynamically:
# s3_register("dplyr::dplyr_reconstruct", "my_class")
# Set options (with package prefix):
op <- options()
op_mypackage <- list(
mypackage.verbose = FALSE,
mypackage.api_url = "https://api.example.com"
)
toset <- !(names(op_mypackage) %in% names(op))
if (any(toset)) options(op_mypackage[toset])
invisible()
}
.onAttach <- function(libname, pkgname) {
# Run when package is attached (library() call)
# OK to show startup messages here
packageStartupMessage(
"Welcome to mypackage version ",
utils::packageVersion("mypackage")
)
# Check for API key:
if (is.null(get_api_key())) {
packageStartupMessage(
"No API key found. Set one with set_api_key()"
)
}
invisible()
}
.onUnload <- function(libpath) {
# Cleanup when package is unloaded
# Close connections, free resources, etc.
library.dynam.unload("mypackage", libpath)
}
```
**Key differences:**
- `.onLoad()`: Always runs (even with `loadNamespace()`), no messages
- `.onAttach()`: Only with `library()`, messages OK
- Use `.onLoad()` for setup, `.onAttach()` for user communication
## File Organization
### Anti-Pattern: One Function Per File
**Problem**: Creates too many small files, hard to navigate.
```
# AVOID (unless functions are very large):
R/
├── add.R
├── subtract.R
├── multiply.R
├── divide.R
├── mean.R
├── median.R
└── mode.R
```
### Anti-Pattern: All Code in One File
**Problem**: Creates one huge file, hard to maintain.
```
# AVOID:
R/
└── functions.R # 5000 lines!
```
### Good Pattern: Logical Grouping
```
R/
├── mypackage-package.R # Package docs and imports
├── arithmetic.R # add(), subtract(), multiply(), divide()
├── statistics.R # mean(), median(), mode(), sd()
├── data.R # Data documentation
├── utils.R # Internal utilities
└── zzz.R # .onLoad, .onAttach
```
**Grouping strategies:**
- By feature/domain
- By class (if using S3/R6)
- By workflow step
- utils.R for shared internal functions
### File Naming Conventions
```
R/
├── aaa-imports.R # Loaded first (package-level imports)
├── class-model.R # Class definition
├── model-methods.R # Methods for model class
├── model-utils.R # Utilities for model class
├── generics.R # Generic function definitions
عرض على GitHub