Open-source libraries used

1 libraries are bundled into this tool's code.

R Cheatsheet — Concise Reference

A cheatsheet for R 4.4 syntax, data structures and the most-used base R / tidyverse functions, covering about 80% of everyday scenarios.

R

R R 4.4

R (GNU R / interactive interpreter) · Functional · Vectorized · OO (S3 / S4 / R6) · Dynamic

Recommended Learning Path

Start by running Rscript and using assignment (<-) → master vectors, data.frame and the other data structures → write control flow with if/for and the apply family → learn functions, closures and the pipe |> → dig deeper into apply-style collection operations → understand S3/R6 object orientation and error handling → then pick up file I/O, parallelism, networking and regular expressions as needed. The FAQ section is worth revisiting to avoid pitfalls; package management and renv are covered in the build chapter.

1.Hello World and the runtime

Running with Rscript, the REPL, printed output, and the help system.

Minimal program

R evaluates expressions one at a time. `print` displays an object; `cat` writes raw output. Scripts run via `Rscript`.

1
2
3
4
5
print('Hello, world!') # print to the console
cat('Hello, world!\n') # raw output, no quotes
# Save it as hello.R, then run:
# $ Rscript hello.R
# or source('hello.R') inside R / RStudio

Assignment and output

Use `<-` (or `=`) to assign. `print` shows object structure; `cat` is good for concatenated output. `message` writes to stderr.

1
2
3
4
5
x <- 42 # assignment (<- preferred)
y = 42 # = is legal too, rarely used by style
print(x) # shows [1] 42
cat('x =', x, '\n') # joined output: x = 42
message('这是提示') # written to stderr

Running Rscript

Rscript runs a script non-interactively — ideal for batch jobs and the command line. `source` runs a script inside an interactive session.

1
2
3
4
5
6
7
8
9
10
# Script hello.R:
# print('hello')
# Run the script:
# $ Rscript hello.R
# Run it with arguments:
# $ Rscript hello.R a b
# Run a file inside a session:
source('hello.R')
# Evaluate a string directly:
eval(parse(text = '1 + 1'))

Command-line arguments

`commandArgs(trailingOnly = TRUE)` returns the arguments that follow the script name; all values are strings.

1
2
3
4
5
6
7
8
9
# Run: Rscript app.R a b c
args <- commandArgs(trailingOnly = TRUE)
print(args) # [1] "a" "b" "c"
# Whether the script name is kept (if FALSE, args[1] is the script path):
commandArgs() # includes Rscript and the script path
# Use args[1] as the input path:
if (length(args) > 0) {
input <- args[1]
}

Printed output

`print` shows the object and its structure; `cat` concatenates without quotes; `sprintf` formats. `invisible` suppresses auto-printing.

1
2
3
4
5
6
7
8
x <- c(1, 2, 3)
print(x) # [1] 1 2 3
cat(x, sep = ', ', '\n') # 1, 2, 3
sprintf('Pi = %.2f', pi) # "Pi = 3.14"
# Expressions auto-print (top level only):
1 + 1 # [1] 2
# A function's last expression is returned and printed:
invisible(1) # not printed

Help docs

`?` opens the help page; `??` does a full-text search. `args` lists the formal arguments; `example` runs the examples.

1
2
3
4
5
6
7
8
?mean # help page for mean
??regression # full-text search
args(mean) # formal argument list
example(mean) # run the built-in examples
apropos('dist') # find objects whose name has dist
help.search('聚类') # search by topic
# View the source:
mean # print the function body

Loading packages

`library` attaches a package to the search path; `require` returns a logical. `::` calls a function from a specific package.

1
2
3
4
5
6
7
8
9
10
11
library(dplyr) # load a package
library('ggplot2') # the string form works too
if (!requireNamespace('pkg', quietly = TRUE)) {
install.packages('pkg')
}
# Explicit namespace call:
dplyr::select(df, x)
# See what is loaded:
search()
# List installed packages:
rownames(installed.packages())

Running a script

`source` runs the file in the current session, defining its functions and variables. `echo = TRUE` shows each line as it executes.

1
2
3
4
5
6
7
source('utils.R') # run utils.R
source('utils.R', encoding = 'UTF-8')
# Echo each line and its result:
source('demo.R', echo = TRUE)
# Common use: load your own function collection:
# utils.R defines f <- function(x) x * 2
f(5) # 10

2.Variables and constants

Assignment, basic types, `NA`/`NULL`, and type coercion.

Assignment operators

`<-` is the idiomatic assignment; `=` also works. `<<-` writes to an outer variable from inside a function. Use `assign()` for fully global assignment.

1
2
3
4
5
6
7
8
9
10
11
x <- 5 # preferred
y = 5 # also legal
x <- y <- z <- 1 # chained assignment, all three are 1
# Change an outer variable from inside a function:
f <- function() {
g <<- 99 # writes to the global environment
}
# Assign by name:
assign('name', 'Rex')
name # "Rex"
# Difference from =: = does not reach nested function scopes

Basic types

Atomic vectors come in `numeric`, `integer`, `character`, and `logical`. Append `L` to mark an integer. `typeof()` reveals the underlying storage type.

1
2
3
4
5
6
7
8
9
10
x <- 3.14 # numeric (double)
i <- 1L # integer (L suffix)
s <- 'hi' # character
b <- TRUE # logical
typeof(x) # "double"
typeof(i) # "integer"
# Length: every scalar is a length-1 vector:
length(s) # 1
# Compound type:
typeof(1:5) # "integer"

NA and NULL

`NA` marks a missing value; `NULL` represents an empty object. Check with `is.na()` / `is.null()`. `NaN` is Not-a-Number; `Inf` is infinity.

1
2
3
4
5
6
7
8
9
10
11
12
13
v <- c(1, NA, 3)
is.na(v) # FALSE TRUE FALSE
any(is.na(v)) # TRUE
# NULL means the object does not exist:
x <- NULL
is.null(x) # TRUE
# Special numeric values:
0 / 0 # NaN
1 / 0 # Inf
is.nan(NaN) # TRUE
is.finite(Inf) # FALSE
# Ignore NA when summarising:
mean(c(1, NA), na.rm = TRUE) # 1

Type coercion

`as.*()` performs an explicit coercion. `c()` auto-coerces mixed types to the most general one. Use `is.*()` to test.

1
2
3
4
5
6
7
8
9
10
11
12
as.numeric('42') # 42
as.character(42) # "42"
as.logical(1) # TRUE
as.factor(c('a', 'b')) # a factor
# Mixing types in c() coerces automatically:
c(1, 'a') # "1" "a" (becomes character)
c(1, TRUE) # 1 1 (becomes numeric)
# A failed conversion yields NA:
as.numeric('abc') # NA (with a warning)
# Safe conversion + check:
x <- suppressWarnings(as.numeric('abc'))
is.na(x) # TRUE

Creating vectors

`c()` concatenates; `:` generates a range; `seq()` controls the step; `rep()` repeats. Assign `names()` to label elements.

1
2
3
4
5
6
7
8
9
10
c(1, 2, 3) # combine
1:10 # 1 2 ... 10
seq(0, 1, by = 0.25) # 0 0.25 ... 1
seq(1, 10, length.out = 5) # 5 evenly spaced points
rep(1, 3) # 1 1 1
rep(c('a', 'b'), each = 2) # a a b b
# Named elements:
x <- c(a = 1, b = 2)
names(x) # "a" "b"
x['a'] # 1

Naming conventions

Names allow letters, digits, dots, and underscores, but may not start with a digit. The dot has no operator meaning in R.

1
2
3
4
5
6
7
8
9
10
11
12
my_var <- 1 # underscore
my.var <- 2 # a dot is legal
.var <- 3 # leading dot
# Cannot start with a digit:
# 1var <- 1 # syntax error
# Reserved words cannot be variable names:
# if <- 1 # error
# camelCase or snake_case, just stay consistent:
totalCount <- 10
total_count <- 20
# In R the dot is an ordinary character:
a.b <- 1 # legal

Managing variables

`ls()` lists variables; `rm()` removes them. `exists()` checks for presence. `rm(list = ls())` empties the environment.

1
2
3
4
5
6
7
8
9
10
11
x <- 1; y <- 2
ls() # "x" "y"
exists('x') # TRUE
rm(x) # remove x
rm(list = ls()) # clear the current environment
# Remove only names matching a pattern:
rm(list = ls(pattern = '^tmp'))
# Inspect an object's structure:
str(y) # num 2
# Environment info:
ls(all.names = TRUE) # includes hidden objects

Built-in constants

R ships handy constants: `pi`, `letters`, `month.name`, plus `.Machine` for platform particulars.

1
2
3
4
5
6
7
8
9
10
11
pi # 3.141593
letters # a b ... z
LETTERS # A B ... Z
letters[1:3] # "a" "b" "c"
month.name # January ...
month.abb # Jan Feb ...
# Machine precision and friends:
.Machine$double.eps # 2.22e-16
.Machine$integer.max # 2147483647
# Other built-ins:
date() # current date-time as a string

Scope basics

R uses lexical scoping: a function searches enclosing environments outward. A local name shadows the global. `get()` reads from a chosen environment.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- 'global'
f <- function() {
x <- 'local' # shadows the global
x
}
f() # "local"
# Without a local assignment the global is used:
g <- function() x
g() # "global"
# Read from an explicit environment:
get('x', envir = .GlobalEnv)
# The environment chain:
parent.env(globalenv())

3.Data types and structures

Vectors, factors, matrices, lists, data frames, and tibbles — R's data-structure hierarchy.

Vectors

Atomic vectors are homogeneous and one-dimensional — R's most basic structure. Build with `c()`; indexing starts at 1.

1
2
3
4
5
6
7
8
9
10
11
12
v <- c(1, 2, 3)
v[1] # 1 (indexing starts at 1)
v[c(1, 3)] # 1 3
v[-1] # drop the first element
v > 1 # FALSE TRUE TRUE
v[v > 1] # logical filter: 2 3
length(v) # 3
# Integer sequence:
1:5
# Access by name:
x <- c(a = 1, b = 2)
x['a']

Factors

Factors store categorical variables with fixed `levels`. Ordered factors capture order. Create with `as.factor()` / `factor()`.

1
2
3
4
5
6
7
8
9
10
11
f <- factor(c('低', '中', '高'))
levels(f) # "低" "中" "高"
table(f) # counts
# Fix the category order:
f2 <- factor(c('低', '高'),
levels = c('低', '中', '高'))
# Ordered factor:
of <- ordered(c('低', '高'),
levels = c('低', '中', '高'))
of[2] > of[1] # TRUE
# Turning a factor back into numbers needs care (see FAQ):

Matrices

`matrix()` builds a 2D homogeneous array. `byrow` controls the fill direction. Set row/column names via `dimnames`.

1
2
3
4
5
6
7
8
9
10
11
12
m <- matrix(1:6, nrow = 2, ncol = 3)
m # 2 rows, 3 columns
m[1, 2] # row 1, column 2
m[1, ] # row 1
# Fill by row:
matrix(1:6, nrow = 2, byrow = TRUE)
# Row / column operations:
rowSums(m); colMeans(m)
# Dimensions:
dim(m) # 2 3
# Names:
dimnames(m) <- list(c('r1', 'r2'), c('a', 'b', 'c'))

Arrays

`array()` makes a homogeneous structure of any dimension. `dim` sets the length along each axis. Useful for multi-dimensional tensors.

1
2
3
4
5
6
7
8
9
10
11
a <- array(1:24, dim = c(2, 3, 4))
dim(a) # 2 3 4
a[1, 2, 3] # read one slice value
# Indexing drops dimensions:
a[1, , ] # a 3x4 matrix
# Sum along a dimension:
apply(a, 3, sum) # sum each third-dimension slice
# Reshape:
dim(a) <- c(6, 4) # reshaped to 6x4
# Relation to matrix:
is.matrix(a) # TRUE (when dim has length 2)

Lists

`list()` is heterogeneous and nestable. Use `$` or `[[` to extract an element; `[` returns a sub-list. `length()` counts elements.

1
2
3
4
5
6
7
8
9
10
11
l <- list(name = 'Rex', age = 5, tags = c('a', 'b'))
l$name # "Rex"
l[[2]] # 5
l[1] # a sub-list (holding name)
length(l) # 3
names(l) # "name" "age" "tags"
# Nested list:
l2 <- list(a = list(x = 1), b = 2)
l2$a$x # 1
# Walk it recursively:
unlist(l2) # flatten into a vector

Data frames

A `data.frame` is tabular data with heterogeneous columns. Default `stringsAsFactors = FALSE` since R 4.0. Inspect with `str()` / `head()`.

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(
name = c('alice', 'bob'),
age = c(30, 25),
admin = c(TRUE, FALSE)
)
df$age # take a column (a vector)
df[2, ] # row 2
df[['name']] # take a column (exact name)
nrow(df); ncol(df) # 2 3
str(df) # structure overview
head(df, 2) # first 2 rows
# Add a column:
df$score <- c(88, 92)

tibble

A `tibble` is the tidyverse's modern data frame: lazy columns, friendly printing, and never coercing strings to factors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(tibble)
tb <- tibble(
name = c('a', 'b'),
value = c(1, 2)
)
tb # prints nicely, shows types
# A column may refer to earlier ones:
tb2 <- tibble(x = 1:3, y = x * 2)
# No factor conversion:
class(tb$name) # character
# Convert to and from data.frame:
df <- as.data.frame(tb)
tb3 <- as_tibble(df)
# Extract:
tb$value; tb[['value']]

Attributes

`attributes()` carry metadata: `names`, `dim`, `class`, etc. `attr()` reads/sets one attribute. `structure()` builds an object with attributes in a single call.

1
2
3
4
5
6
7
8
9
10
11
12
x <- 1:3
attr(x, 'unit') <- 'cm' # custom attribute
attributes(x) # list them all
attr(x, 'unit') # "cm"
# structure builds it in one step:
y <- structure(1:3, unit = 'cm')
# Common built-in attributes:
dim(m); names(df); class(x)
# Remove attributes:
attributes(x) <- NULL
# Check an object's class:
class(c(1, 2)) # "numeric"

Type checks

`is.*()` tests the type; `class()`/`typeof()`/`mode()` reveal it. `as.*()` converts. Use things like `is.data.frame` to branch.

1
2
3
4
5
6
7
8
9
10
11
12
is.numeric(1) # TRUE
is.integer(1L) # TRUE
is.character('a') # TRUE
is.logical(TRUE) # TRUE
is.list(list()) # TRUE
is.data.frame(df) # TRUE
is.matrix(m) # TRUE
# class vs typeof:
class(data.frame()) # "data.frame"
typeof(list()) # "list"
# Test null / na:
is.null(NULL); is.na(NA)

4.References and object semantics

R has no raw pointers: copy-on-modify gives value semantics; environments provide reference semantics.

Copy-on-modify

R uses copy-on-modify: assignments share the underlying data, and a real copy happens only on modification. Aliasing big objects is nearly free.

1
2
3
4
5
6
7
8
9
10
11
x <- 1:1e6
y <- x # no copy, the data is shared
# Modifying y is what triggers the copy:
y[1] <- 99
# x is unaffected:
x[1] # 1
# tracemem shows when copies happen:
tracemem(x)
y2 <- x # shared, not copied
y2[1] <- 1 # the copy happens here
untracemem(x)

tracemem for copy tracking

`tracemem()` tags an object; R prints the address change whenever a real copy happens. It helps diagnose unexpected copies and memory overhead.

1
2
3
4
5
6
7
8
9
10
x <- 1:1e6
tracemem(x) # start tracing
y <- x # no output: nothing copied
y[1] <- 0 # prints tracemem: a copy
untracemem(x) # stop tracing
# Replacing a whole column of a big data frame copies it:
df <- data.frame(a = 1:1e5, b = rnorm(1e5))
tracemem(df)
df$a <- df$a + 1 # the whole frame is copied
untracemem(df)

Environments as references

An `environment` is R's reference object: passing it doesn't copy, and modifications inside a function take effect immediately — the closest thing R has to a pointer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
e <- new.env() # create an environment
e$x <- 1
e[['y']] <- 2
# Modify it inside a function (no <<- needed):
inc <- function(env) {
env$x <- env$x + 1
}
inc(e)
e$x # 2 (reference semantics apply)
# Read:
get('x', envir = e)
ls(e) # "x" "y"
# An environment is also a hash table:
e$name <- 'Rex'

Mutable state containers

Environments serve as mutable state containers: counters, caches, accumulators. Avoid polluting the globals by passing state explicitly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
counter <- function() {
e <- new.env()
e$n <- 0
function() {
e$n <- e$n + 1 # mutate the captured environment
e$n
}
}
next_num <- counter()
next_num() # 1
next_num() # 2
# Cache: store results by key
cache <- new.env(hash = TRUE)
cache$key <- list(result = 42)

Shallow vs. deep copies

List assignment is a shallow copy: sub-objects are shared, so editing a nested element copies that level. Deep copies must be implemented explicitly.

1
2
3
4
5
6
7
8
9
10
11
12
l1 <- list(x = 1:3, y = list(a = 1))
l2 <- l1 # shallow copy, sub-objects shared
# Modifying l1's top level copies that level:
l1$x[1] <- 99 # x is copied, y is still shared
# Deep-copy example (recursive):
deep_copy <- function(obj) {
if (is.list(obj)) lapply(obj, deep_copy)
else obj
}
l3 <- deep_copy(l1)
# data.table has an explicit copy():
# dt2 <- copy(dt)

R6 reference classes

R6 classes use reference semantics: objects are passed by reference, and methods mutate the original rather than a copy — well suited for state management.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(R6)
Counter <- R6Class('Counter',
public = list(
n = 0,
increment = function() self$n <- self$n + 1
)
)
c1 <- Counter$new()
c2 <- c1 # a reference to the same object
c2$increment()
c1$n # 1 (c1 changed too)
# R5 / setRefClass has similar reference semantics:
# setRefClass defines fields + methods
# Unlike S3/S4, R6 mutation has no copy-on-modify

data.table references

`data.table` mutates in place with `:=` (reference semantics); `data.frame` is copy semantics. `setDT()` converts to `data.table` in place.

1
2
3
4
5
6
7
8
9
10
11
12
# library(data.table)
dt <- data.table(a = 1:3, b = 4:6)
dt[, c := a + b] # add column c in place, no copy
dt
# Modifying a data.frame copies it:
df <- as.data.frame(dt)
df$d <- 1 # makes a copy
# Set / delete in place:
dt[, d := NULL] # drop a column
setDT(df) # convert to data.table in place
# Explicit copy:
dt2 <- copy(dt) # data.table::copy

Object size

`object.size()` measures one object; `gc()` reports overall memory. `lobstr::obj_size()` accounts for sharing.

1
2
3
4
5
6
7
8
9
object.size(1:1e6) # ~8 MB
format(object.size(1:1e6), units = 'MB')
# Overall memory:
gc() # used / peak (MB)
memory.size() # Windows only
# Real footprint of shared objects:
# lobstr::obj_size(x, y) # shared parts counted once
# Large datasets:
# use data.table to reduce copying

Explicit copies

When you really need an independent copy: recursively copy a list with `lapply()`, copy a `data.table` with `copy()`, and copy a vector with `[]`.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Deep-copy a list:
l <- list(a = 1:3, b = list(x = 1))
l_copy <- lapply(l, function(x) {
if (is.list(x)) lapply(x, function(y) y)
else x
})
# data.table copy:
# dt2 <- copy(dt)
# Vector copy:
v <- c(1, 2, 3)
v2 <- v[] # explicit copy
# Changing v2 leaves v alone:
v2[1] <- 99
v[1] # 1
# Check whether they are shared:
# lobstr::ref(v, v2)

5.Control flow

`if` / `else`, the vectorised `ifelse`, `for` / `while`, `switch`, and logical operators.

if / else

An `if` condition must have length 1. Anything else takes the first element and warns. Place `else` on the same line as the closing brace.

1
2
3
4
5
6
7
8
9
10
11
12
x <- 5
if (x > 0) {
print('正数')
} else if (x == 0) {
print('零')
} else {
print('负数')
}
# Only a length-1 condition is safe:
if (c(TRUE, FALSE)) print('x') # warns and uses the first element
# As an expression:
y <- if (x > 0) 'pos' else 'neg'

Vectorised ifelse

`ifelse()` tests elementwise over a vector and returns a result the same length as the condition — handy for bulk replacement.

1
2
3
4
5
6
7
8
9
10
11
x <- c(-1, 0, 1, 2)
ifelse(x > 0, 'pos', 'non-pos')
# Nested:
ifelse(x > 0, 'pos',
ifelse(x == 0, 'zero', 'neg'))
# Note that ifelse coerces the return type:
ifelse(x > 0, 1L, 0) # both become double
# Keep NA:
ifelse(x > 0, 'pos', NA_character_)
# tidyverse alternative:
# dplyr::case_when(x > 0 ~ 'pos', TRUE ~ 'neg')

for loops

`for` iterates over a vector or sequence. Prefer `seq_along()` for index loops to avoid the empty-sequence trap of `1:length()`.

1
2
3
4
5
6
7
8
9
10
11
for (i in 1:5) print(i)
# Iterate over vector elements:
for (ch in c('a', 'b')) print(ch)
# Iterate by index:
x <- c(10, 20, 30)
for (i in seq_along(x)) {
x[i] <- x[i] * 2
}
x # 20 40 60
# Avoid 1:length(x); it breaks when x is empty
# Prefer vectorisation over loops

while and repeat

`while` tests the condition before each iteration; `repeat` loops unconditionally, relying on `break` to exit. Guard against infinite loops in both.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
n <- 0
while (n < 3) {
n <- n + 1
}
n # 3
# repeat runs at least once:
total <- 0
repeat {
total <- total + 1
if (total >= 5) break
}
total # 5
# Infinite-loop guard: cap the iterations
# Use break/return when the condition may never hold

next and break

`next` jumps to the next iteration; `break` exits the loop — ideal for skipping or early termination.

1
2
3
4
5
6
7
8
9
10
11
12
for (i in 1:10) {
if (i %% 2 == 0) next # skip even numbers
if (i > 7) break # stop once past 7
print(i) # 1 3 5 7
}
# In nested loops break only leaves the inner one:
for (i in 1:3) {
for (j in 1:3) {
if (j == 2) break
cat(i, j, '\n')
}
}

switch

`switch()` dispatches by position or name. A numeric value picks the nth branch; a string matches a named branch; otherwise it returns `NULL`.

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(2, 'a', 'b', 'c') # "b" (by position)
op <- 'add'
switch(op,
add = 1 + 1, # 2
mul = 2 * 3,
'未知'
)
# With no default, no match returns NULL:
switch('nope', add = 1)
# A number out of range returns NULL too:
switch(5, 'a', 'b')
# match.arg validates an argument:
# match.arg(choice, c('a', 'b'))

Vectorisation over loops

R replaces explicit loops with vectorization: operate on whole vectors at once, faster and more concise. Reserve loops for cases that cannot be vectorized.

1
2
3
4
5
6
7
8
9
10
11
x <- 1:1e6
y <- x * 2 # whole-vector multiply, fast
z <- x^2 + sqrt(x)
# Equivalent loop (slow, not recommended):
for (i in seq_along(x)) x[i] * 2
# Common vectorised functions:
sum(x); mean(x); cumsum(x)
which(x > 5) # indices that match
x[x > 5] # the matching values
# Vectorised comparison:
any(x > 5); all(x > 0)

Logical Operators

& and | are vectorized logical operators; && and || evaluate only the first element (short-circuit). any/all summarize.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(TRUE, FALSE, TRUE)
y <- c(FALSE, TRUE, TRUE)
x & y # FALSE FALSE TRUE
x | y # TRUE TRUE TRUE
!x # negate
# Short-circuit versions (single value):
TRUE && FALSE # FALSE
TRUE || stop('不执行') # short-circuits, no error
# Summaries:
any(x); all(x)
# Chained comparisons must be written out:
# x > 1 && x < 5
# Note that && and || use only the first element

Index Iteration

seq_along / seq_len generate indices, rev reverses order. which returns positions. split iterates over groups.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(3, 1, 4, 1, 5)
seq_along(x) # 1 2 3 4 5
seq_len(3) # 1 2 3
# Iterate in reverse:
for (i in rev(seq_along(x))) print(x[i])
# which:
which(x == 1) # 2 4
which.max(x) # 5
# Loop by group:
grp <- split(x, x > 2)
lapply(grp, sum)
# Element-wise output:
# sapply(x, function(v) ...)

6.Functions and Closures

Function definition, arguments, lazy evaluation, variadic arguments, closures, and pipes.

Function Definition

function defines a function; the last expression in the body is the return value. Anonymous functions are used directly as arguments.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
square <- function(x) {
x * x
}
square(5) # 25
# A single expression can be inlined:
add1 <- function(x) x + 1
add1(2) # 3
# Anonymous function:
(function(x) x * 2)(10) # 20
# Functions are objects, so they can be assigned:
f <- add1
f(1) # 2
# Inspect the body:
body(square)

Arguments and Defaults

Function arguments can have default values. Calls may omit argument names and pass by position, or use named arguments. missing checks whether an argument was supplied.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
greet <- function(name, greeting = '你好') {
paste(greeting, name)
}
greet('张三') # 你好 张三
greet('张三', '早上好')
greet(greeting = '嗨', name = '李四')
# Detect a missing argument:
show <- function(x) {
if (missing(x)) return('未提供')
x
}
show() # 未提供
# ... passes arguments through:
f <- function(x, ...) paste(x, ...)
f('a', 'b', 'c')

Lazy Evaluation

R uses lazy evaluation for arguments: evaluated only when used, and unreferenced arguments do not error. force forces early evaluation. This is critical in closures.

1
2
3
4
5
6
7
8
9
10
11
f <- function(a, b) a
f(1, stop('这行不执行')) # 1, b is never used
# Force evaluation:
g <- function(a, b) {
force(b) # evaluate b right away
a
}
g(1, stop('会报错')) # raises the error
# Laziness lets a default argument refer to earlier ones:
h <- function(x, y = x * 2) y
h(5) # 10

Variadic Arguments

... captures any number of arguments. list(...) collects them, do.call dynamically invokes a function with a list.

1
2
3
4
5
6
7
8
9
10
11
12
13
sum_all <- function(...) sum(...)
sum_all(1, 2, 3) # 6
# Collect into a list:
capture <- function(...) list(...)
capture(1, 'a', TRUE)
# do.call: pass arguments dynamically
args <- list(x = 1:3, y = 1:3)
do.call(pmax, args)
# Spread vector elements as arguments:
vals <- c(1, 2, 3)
do.call(sum, as.list(vals)) # 6
# ... forwards to another function:
wrap <- function(...) plot(...)

Return Values

A function returns the last expression. return exits early. invisible returns a value without auto-printing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
f <- function(x) x * 2 # the last expression is returned
f(3) # 6
# return exits early:
g <- function(x) {
if (x < 0) return('负数')
x * 2
}
g(-1) # 负数
# Return several values in a list:
h <- function(x) {
list(sq = x^2, rt = sqrt(x))
}
h(4) # a list holding sq and rt
# A return value that is not printed:
invisible(42)

Closures

A function captures the environment in which it was created and can carry private state. Factory functions generate new functions with state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
make_adder <- function(n) {
function(x) x + n # the closure captures n
}
add5 <- make_adder(5)
add5(3) # 8
# Counter closure:
counter <- local({
n <- 0
function() {
n <<- n + 1 # update n in the environment
n
}
})
counter(); counter() # 1 2
# The environment carries the closure:
environment(add5)

Anonymous Functions

Anonymous functions are function literals, commonly used with the apply family and purrr. R 4.1+ supports the \(x) shorthand.

1
2
3
4
5
6
7
8
9
10
11
lapply(1:3, function(x) x^2) # 1 4 9
sapply(1:3, function(x) x * 2)
# \\(x) shorthand (R >= 4.1):
lapply(1:3, \(x) x^2)
# Call it directly:
(function(x) x + 1)(5) # 6
# purrr pipe style:
# purrr::map(1:3, ~ .x * 2)
# Anonymous functions while sorting:
sort(c(3, 1, 2))
sort(c('b', 'a'), decreasing = TRUE)

Higher-Order Functions

Functions can be passed as arguments to other functions and can also return functions. Map / Reduce / Filter are built-in higher-order functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
Filter(is.numeric, list(1, 'a', 2))
Reduce(`+`, 1:5) # 15
Map(function(x, y) x + y, 1:3, 4:6)
# A function that returns a function (currying):
curry_add <- function(a) {
function(b) a + b
}
curry_add(10)(5) # 15
# Common pattern: apply a function to every data frame column
sapply(df, mean, na.rm = TRUE)
# Roll your own higher-order function:
apply_twice <- function(f, x) f(f(x))
apply_twice(function(x) x + 1, 5) # 7

Pipes

|> is the native pipe that passes the left-hand result as the first argument to the right-hand function, turning nested calls into a linear flow. %>% is the magrittr version.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
x <- 1:10
x |> sum() # 55
# Chained pipes:
1:10 |> mean() |> round(2)
# Use a placeholder to reach a non-first argument:
1:10 |> paste(collapse = '-') # wrong on purpose
# Native pipe placeholder:
1:10 |> paste(_, collapse = '-')
# tidyverse style:
df |> dplyr::filter(age > 18) |>
dplyr::select(name) |>
dplyr::arrange(name)
# magrittr pipe with the . placeholder:
library(magrittr)
1:10 %>% sum()

7.Strings

Character vectors, concatenation, substring, splitting, regex, and formatting.

String Basics

Character vectors are quoted; double and single quotes are equivalent. nchar returns length, [] extracts a character. Backslashes need escaping.

1
2
3
4
5
6
7
8
9
10
11
12
s <- 'hello'
"hi" # equivalent
toupper('abc') # "ABC"
tolower('ABC') # "abc"
nchar('中文') # 2 (character count)
# Backslash escapes:
cat('a\tb') # a b
# Raw string:
raw_s <- r"(C:\path\file)"
# Concatenate:
paste('a', 'b') # "a b"
paste0('a', 'b') # "ab"

Concatenation

paste uses a space as the default separator; paste0 uses no separator. collapse joins an entire vector into a single string. Concatenation is vectorized.

1
2
3
4
5
6
7
8
9
10
11
12
paste('a', 'b', 'c') # "a b c"
paste0('a', 'b') # "ab"
paste('a', 'b', sep = '-') # "a-b"
# collapse: join a vector into one string
paste(1:3, collapse = ',') # "1,2,3"
# Vectorised combination:
paste(letters[1:3], 1:3) # "a 1" "b 2" "c 3"
# Recycling shorter arguments:
paste(letters[1:2], 1:3) # recycled to length 3
# Characters vs bytes:
nchar('你好') # 2
nchar('你好', type = 'bytes') # 6

Substring

substr / substring extract by position; [] with brackets takes a single character. strsplit splits.

1
2
3
4
5
6
7
8
9
10
s <- 'hello world'
substr(s, 1, 5) # "hello"
substring(s, 7) # "world" (to the end)
# Take single characters:
strsplit(s, '')[[1]] # one character at a time
# Replace a substring:
substr(s, 7, 11) <- 'R!'
s # "hello R!"
# Take several ranges by position:
substr('abcdef', c(1, 4), c(2, 6))

Splitting

strsplit splits by a delimiter into a list. unlist flattens. A regex can also serve as the delimiter.

1
2
3
4
5
6
7
8
9
10
11
s <- 'a,b,c'
strsplit(s, ',') # a list ["a" "b" "c"]
unlist(strsplit(s, ',')) # a vector
# Vectorised split:
strsplit(c('a-b', 'c-d'), '-')
# Regex separator:
strsplit('a1b22c', '[0-9]+')
# Fixed-string match (not a regex):
strsplit('a.b.c', '.', fixed = TRUE)
# Join it back:
paste(unlist(strsplit(s, ',')), collapse = ';')

Regex Functions

grep / grepl match, gsub / sub replace. All operate vectorized element-wise. See the regex chapter for details.

1
2
3
4
5
6
7
8
9
10
x <- c('apple', 'banana', 'cherry')
grepl('^a', x) # TRUE FALSE FALSE
grep('a', x) # indices 1 2
# Replace:
gsub('a', 'o', x) # opple bonono cherory
sub('a', 'o', x) # replaces the first only
# Extract matches:
regmatches(x, gregexpr('a', x))
# Case:
grepl('APPLE', x, ignore.case = TRUE)

Formatting

sprintf mimics C printf formatting. %s for strings, %d for integers, %f for floats. formatC / round control numbers.

1
2
3
4
5
6
7
8
9
10
11
sprintf('%s is %d', 'R', 4) # "R is 4"
sprintf('%.2f', pi) # "3.14"
sprintf('%5.1f', pi) # " 3.1"
sprintf('%05d', 42) # "00042"
# Escaping the percent sign:
sprintf('100%%')
# Vectorised:
sprintf('x%d', 1:3) # "x1" "x2" "x3"
# Number formatting:
format(pi, digits = 3)
formatC(12345, big.mark = ',')

Case and Trimming

toupper / tolower convert case, trimws removes whitespace. chartr performs character translation. Commonly used when processing tool names.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
toupper('hello') # "HELLO"
tolower('HELLO') # "hello"
# Capitalise the first letter:
to_title <- function(s) {
paste0(toupper(substr(s, 1, 1)), substr(s, 2, nchar(s)))
}
to_title('hello') # "Hello"
# Trim whitespace:
trimws(' hi ') # "hi"
trimws('\nhi\t', which = 'both')
# Character-level replacement:
chartr('a-c', 'A-C', 'abc')
# camelCase to underscores:
# gsub('([A-Z])', '_\\L\\1', s, perl = TRUE)

Encoding

R strings are primarily UTF-8. Encoding inspects the encoding, enc2utf8 converts, iconv transcodes.

1
2
3
4
5
6
7
8
9
10
11
12
13
s <- '你好'
Encoding(s) # "UTF-8"
enc2utf8(s) # convert to UTF-8
enc2native(s) # convert to the native encoding
# Encoding conversion:
iconv(s, from = 'UTF-8', to = 'GB18030')
# Byte view:
charToRaw('A') # 41
rawToChar(as.raw(0x41)) # "A"
# Hex string:
paste(as.hexmode(charToRaw('R')), collapse = ' ')
# Read a file with a given encoding:
# readLines('f.txt', encoding = 'UTF-8')

stringr

A tidyverse string-handling package. Functions share a unified str_ prefix: str_detect / str_replace / str_extract.

1
2
3
4
5
6
7
8
9
10
11
12
# library(stringr)
str_detect(c('a1', 'b2'), '[0-9]')
str_replace('ab-cd', '-', '_')
str_replace_all('a-b-c', '-', '')
str_extract('price 99', '[0-9]+')
str_extract_all('a1 b2', '[0-9]')
str_remove('xx-abc', 'xx-')
str_split('a,b', ',')
str_pad('5', 3, pad = '0') # "005"
str_trim(' hi ')
str_to_title('hello world')
str_length('你好') # 2

8.Sets and the apply Family

apply / lapply / sapply for batch iteration, sort, deduplication, set operations, and dplyr data manipulation.

lapply and sapply

lapply applies a function element-wise over a list or vector and returns a list. sapply tries to simplify the result into a vector or matrix.

1
2
3
4
5
6
7
8
9
10
l <- list(a = 1:3, b = 4:5)
lapply(l, sum) # a list, holding $a $b
lapply(l, function(x) x * 2)
# sapply simplifies:
sapply(l, sum) # a=6 b=9 named vector
sapply(1:4, sqrt)
# vapply pins the return type (safer):
vapply(1:4, sqrt, numeric(1))
# unlist flattens the list result:
unlist(lapply(1:3, function(x) x^2))

apply

apply operates along a dimension of a matrix or array. MARGIN=1 is row-wise, 2 is column-wise. The return value is assembled by dimension.

1
2
3
4
5
6
7
8
9
10
m <- matrix(1:9, nrow = 3)
apply(m, 1, sum) # row sums
apply(m, 2, sum) # column sums
apply(m, 1, mean)
apply(m, 2, function(x) x / max(x))
# A 3-d array along its third dimension:
a <- array(1:24, c(2, 3, 4))
apply(a, 3, sum)
# Grouping that returns indices:
apply(m, 1, which.max)

mapply: Multiple Arguments

mapply applies a function in parallel over multiple arguments, corresponding to Map. Shorter arguments are recycled. SIMPLIFY controls simplification.

1
2
3
4
5
6
7
8
9
mapply(paste, c('a', 'b'), c(1, 2))
# The counterpart of pmax and friends:
mapply(max, c(1, 5), c(3, 2)) # 3 5
# Map never simplifies, always a list:
Map(paste, c('a', 'b'), c(1, 2))
# Vectorise over several arguments:
mapply(function(x, y) x^y, 1:3, 1:3)
# Simplify into a matrix:
mapply(function(x, y) c(x, y), 1:2, 3:4, SIMPLIFY = TRUE)

split: Grouping

split splits a vector into a list of groups by a factor. Combined with lapply it performs group-wise summaries, equivalent to group-by.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c(1, 2, 3, 4, 5)
g <- c('a', 'a', 'b', 'b', 'b')
split(x, g) # a list $a $b
# Mean per group:
lapply(split(x, g), mean)
# Split a data frame by a column:
df <- data.frame(grp = c('a', 'a', 'b'), val = 1:3)
split(df, df$grp)
# Summary statistics:
sapply(split(df$val, df$grp), summary)
# tapply: all in one step
tapply(df$val, df$grp, mean)
# The equivalent of dplyr group_by + summarise

Sorting and Ranking

sort sorts a vector, order returns the sort indices, rank returns ranks. decreasing controls the direction. Data frames are sorted by column.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- c(3, 1, 2)
sort(x) # 1 2 3
order(x) # 2 3 1 (indices)
x[order(x)] # sorted
rev(sort(x)) # descending
rank(c(3, 1, 2)) # 3 1 2
# Sort a data frame:
df <- data.frame(age = c(30, 20), name = c('b', 'a'))
df[order(df$age), ]
# Sort by several columns:
df2 <- data.frame(a = c(1, 1, 2), b = c(2, 1, 3))
df2[order(df2$a, df2$b), ]
# dplyr:
# dplyr::arrange(df, desc(age))

Deduplication

unique removes duplicates, duplicated flags duplicate entries. Among duplicate rows, the first is kept. all.equal compares vectors.

1
2
3
4
5
6
7
8
9
10
11
12
x <- c(1, 2, 1, 3, 2)
unique(x) # 1 2 3
duplicated(x) # FALSE FALSE TRUE FALSE TRUE
!duplicated(x) # keep the first occurrence
x[!duplicated(x)]
# Drop duplicate data frame rows:
df <- data.frame(a = c(1, 1, 2), b = c(1, 1, 3))
unique(df)
# Count occurrences:
table(x)
# dplyr:
# dplyr::distinct(df, a, .keep_all = TRUE)

Set Operations

union / intersect / setdiff are set operations. %in% tests membership. After unique, take intersections, unions, and differences.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a <- c(1, 2, 3); b <- c(2, 3, 4)
union(a, b) # 1 2 3 4
intersect(a, b) # 2 3
setdiff(a, b) # 1
setdiff(b, a) # 4
# Membership test:
2 %in% a # TRUE
# A union that keeps duplicates:
c(a, b)
# Equality comparison:
identical(a, b)
setequal(a, c(3, 2, 1)) # TRUE (order ignored)
# Filter with %in%:
a[a %in% b] # 2 3

List Operations

Combine lists, flatten, and rename in bulk. unlist recursively flattens, do.call binds into an array. purrr provides typed operations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
l1 <- list(a = 1); l2 <- list(b = 2)
c(l1, l2) # combine
unlist(l1) # named vector
# Apply a function to each element:
lapply(l1, function(x) x + 1)
# Rename list elements:
names(l) <- c('x', 'y')
# Filter elements by condition:
l <- list(1, 'a', TRUE)
Filter(is.numeric, l)
# Flatten one level:
list_of_lists <- list(list(1, 2), list(3))
unlist(list_of_lists, recursive = TRUE)
# purrr:
# purrr::map(l, ~ .x * 2)
# purrr::keep(l, is.numeric)

dplyr Data Manipulation

select / filter / mutate / arrange / summarise manipulate data frames in a pipeline; group_by groups. Core tidyverse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# library(dplyr)
df <- data.frame(
name = c('a', 'b', 'c'),
age = c(25, 35, 40),
salary = c(50, 60, 80)
)
df |> filter(age > 30)
df |> select(name, salary)
df |> mutate(salary2 = salary * 2)
df |> arrange(desc(age))
# Group and summarise:
df |> group_by(age > 30) |>
summarise(avg = mean(salary))
# Deduplicate:
df |> distinct(age)
# Add a computed column, then filter:
df |> mutate(ratio = salary / age) |>
filter(ratio > 1.5)

9.Memory Management

Garbage collection, object size, copy cost, preallocation, and performance profiling.

Garbage Collection

R uses automatic reference counting plus cyclic GC. gc triggers collection manually and reports. Call it after large-data loops.

1
2
3
4
5
6
7
8
9
10
11
gc() # force collection, returns stats
# gc reports Ncells/Vcells used and peak:
gc(reset = TRUE) # reset the peak stats
# Set big objects to NULL once done:
big <- 1:1e8
big <- NULL # now collectable
gc()
# Inspect the gc trigger thresholds:
gc(verbose = TRUE) # print collection details
# Memory usage helpers:
ls() # list the current objects

Object size

object.size inspects the byte size of a single object. format converts to a readable unit. Estimate memory for large datasets.

1
2
3
4
5
6
7
8
9
10
11
object.size(1:1e6)
format(object.size(1:1e6), units = 'MB')
# List size:
object.size(list(1:1e5, 1:1e5))
# Data frame:
df <- data.frame(x = 1:1e5, y = rnorm(1e5))
format(object.size(df), units = 'MB')
# Real footprint of shared objects:
# lobstr::obj_size(x, y)
# Rough estimate for large datasets:
# a 1e7-row data frame is a few hundred MB

Copy Cost

R's value semantics copy the entire object on modification, so mutating large objects is expensive. Avoid frequent in-place edits to large vectors.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- 1:1e7
# Whole-object arithmetic is fast (vectorised):
y <- x * 2
# Element-by-element updates inside a loop:
for (i in 1:1000) x[i] <- x[i] + 1 # triggers lots of copying
# Preallocate instead:
out <- numeric(1e5) # allocate first
for (i in seq_along(out)) out[i] <- i
# Avoid growing with c() inside a loop:
# res <- c() # slow
# Vectorise or preallocate
# Check whether a copy happened:
# tracemem(x)

Preallocation

Allocate a result vector of the final length first, then fill it in a loop, avoiding repeated concatenation and copying. Preallocate numeric/character vectors.

1
2
3
4
5
6
7
8
9
10
11
12
13
n <- 1e4
# Preallocate a numeric vector:
out <- numeric(n)
for (i in 1:n) out[i] <- i^2
# Preallocate characters:
res <- character(n)
# Preallocate a list:
results <- vector('list', n)
for (i in seq_len(n)) results[[i]] <- i * 2
# What to avoid (slow):
# res <- c(); for (i in 1:n) res <- c(res, i)
# Ideal: fully vectorised
sq <- (1:n)^2

Vectorization and Performance

Prefer whole-vector operations. Row-binding with rbind is slow; use do.call(rbind, list) or data.table. Run microbenchmarks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- 1:1e6
# Fast: the whole vector
system.time(y <- x * 2 + sin(x))
# Slow: a loop
# for (i in seq_along(x)) y[i] <- x[i] * 2
# Accumulating with rbind is slow:
# for (...) df <- rbind(df, row) # bad
# The right way: collect a list, then do.call
rows <- lapply(1:100, function(i) data.frame(i = i))
big <- do.call(rbind, rows)
# Timing:
system.time(mean(x))
# Microbenchmark:
# microbenchmark::microbenchmark(a, b)

Memory Limits

memory.limit (Windows) inspects or adjusts the limit. object.size checks size. ulimit affects the session.

1
2
3
4
5
6
7
8
9
10
11
memory.limit() # Windows memory cap in MB
# You may need:
# memory.limit(size = 8192)
# Currently used:
memory.size()
# Other platforms have no memory.limit:
# monitor with system resource tools
# Inspect the large objects:
ls() |> sapply(function(x) object.size(get(x)))
# Sort to see the biggest:
sort(sapply(ls(), function(x) object.size(get(x))), decreasing = TRUE)[1:5]

Profiling

Rprof records function-call timings. summaryRprof summarizes. system.time gives coarse timing. profvis visualizes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Rprof('prof.out')
slow <- function() {
for (i in 1:1e5) sqrt(i)
}
slow()
Rprof(NULL)
summaryRprof('prof.out')
# Simple timing:
system.time(slow())
# Finer timing:
# tictoc::tic(); slow(); tictoc::toc()
# Visualisation:
# profvis::profvis(slow())
# Benchmark comparison:
# bench::mark(a, b)

data.table Memory

data.table uses reference semantics to reduce copies: := modifies in place, setDT converts, copy clones explicitly. Saves memory on large datasets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(data.table)
dt <- data.table(a = 1:1e5, b = rnorm(1e5))
# Add / modify a column in place (no copy):
dt[, c := a * 2]
# The set family:
setkey(dt, a) # sort key
setorder(dt, a) # sort in place
# Avoid copying when subsetting:
sub <- dt[a > 100] # copies the subset
# Explicit copy:
copy(dt)
# Share a column by reference:
# set(dt, j = 'd', value = dt$a)
# Memory comparison:
# replacing a df column copies the whole frame, dt does not

10.Object-Oriented Programming (S3 / S4 / R6)

S3 simple generics, S4 formal classes, and R6 reference classes: three OO systems, each with its own role.

S3 Class Basics

S3 is R's lightweight OO: a class attribute plus generic functions. unclass reveals the underlying object. Most commonly used and lightweight.

1
2
3
4
5
6
7
8
9
10
11
12
x <- 1:3
class(x) # "integer"
# Custom class:
obj <- structure(list(a = 1), class = 'myclass')
class(obj) # "myclass"
# Generic dispatch:
print(obj) # calls print.myclass
# List a generic's methods:
methods('print')
# Check the dispatch target:
class(1:3)
unclass(1:3) # drop the class attribute

S3 Methods

Defining a function named class.method implements a generic. UseMethod dispatches. NextMethod calls the parent method.

1
2
3
4
5
6
7
8
9
10
11
12
shape <- function(x) UseMethod('shape')
shape.default <- function(x) paste('default:', class(x)[1])
shape.circle <- function(x) paste('圆,半径', x$r)
# Construct an object:
c1 <- structure(list(r = 3), class = 'circle')
shape(c1) # 圆,半径 3
shape(1:3) # default
# Override print:
print.circle <- function(x) cat('Circle r =', x$r, '\n')
print(c1)
# List every method:
methods('shape')

S3 Constructors

Custom classes should provide a constructor and validation. structure sets the class in one step. print / summary customize output.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new_point <- function(x, y) {
stopifnot(is.numeric(x), is.numeric(y))
structure(list(x = x, y = y), class = 'point')
}
p <- new_point(1, 2)
# A print method:
print.point <- function(obj, ...) {
cat('point(', obj$x, ',', obj$y, ')\n')
}
print(p)
# A summary method:
summary.point <- function(obj, ...) {
cat('均值:', mean(c(obj$x, obj$y)), '\n')
}
summary(p)
# Generic + validation:
# the new_ prefix is a tidyverse convention

S4 Classes

S4 is formal OO: setClass defines slots, validity validates, setGeneric / setMethod define generics.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setClass('Person',
slots = c(name = 'character', age = 'numeric'),
validity = function(obj) {
if (obj@age < 0) 'age 不能为负' else TRUE
}
)
p <- new('Person', name = 'Rex', age = 5)
p@age # 5
slot(p, 'name') # "Rex"
# Generic:
setGeneric('describe', function(x) standardGeneric('describe'))
setMethod('describe', 'Person', function(x) paste(x@name, x@age))
describe(p)
# Check:
isS4(p)
# Inheritance:
# setClass('Student', contains = 'Person')

R6 Classes

R6 is an encapsulated class with reference semantics; access members with self$ inside methods. Its object-oriented model feels closer to other languages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# library(R6)
Animal <- R6Class('Animal',
public = list(
name = NULL,
initialize = function(name) self$name <- name,
speak = function() paste(self$name, '叫')
)
)
a <- Animal$new('狗')
a$speak() # "狗 叫"
# Private members:
Class <- R6Class('Class',
public = list(init = NULL),
private = list(hidden = 42)
)
# Inheritance:
Dog <- R6Class('Dog',
inherit = Animal,
public = list(speak = function() paste(self$name, '汪汪'))
)
d <- Dog$new('旺财')
d$speak()

Generic dispatch

Dispatch uses the object's class attribute; UseMethod finds the matching method by class. Legacy classes (S3) are tried one layer at a time.

1
2
3
4
5
6
7
8
9
10
11
12
13
f <- function(x) UseMethod('f')
f.default <- function(x) '默认'
f.numeric <- function(x) '数字'
f.factor <- function(x) '因子'
f(1L) # 数字
f(factor('a')) # 因子
f('a') # 默认
# Multiple dispatch (S4):
# setMethod(..., signature = c('x', 'y'))
# View the method dispatch table:
methods('mean')
# class may be a vector:
class(x) <- c('myclass', 'numeric')

Inheritance

S3 inherits through the class attribute (NextMethod chain), S4 uses contains, R6 uses inherit.

1
2
3
4
5
6
7
8
9
10
11
12
13
# S3 inheritance:
structure(list(a = 1), class = c('sub', 'base'))
# NextMethod calls the parent method:
f.base <- function(x) 'base 实现'
f.sub <- function(x) paste('sub:', NextMethod())
f(structure(list(), class = c('sub', 'base')))
# R6 inheritance was covered in the r6 topic:
# use super$ to reach a parent method
# S4 inheritance:
# setClass('Base')
# setClass('Derived', contains = 'Base')
# Check inheritance:
# inherits(obj, 'base')

Comparing the three systems

S3 is lightweight and used by most packages; S4 for rigorous definitions; R6 with reference semantics fits stateful objects. In practice the three are often mixed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# S3: class attribute + generics, no declaration needed
# fast, unenforced, used by most packages
# S4: setClass with strict slots and validation
# suits formal APIs and large frameworks
# R6: reference semantics, encapsulated methods
# suits mutable state and modelled objects
# Seen in the wild:
# ggplot2 uses S3;
# Bioconductor uses S4;
# many newer packages use R6
# How to choose:
# S3 for simple data objects,
# S4 for complex formal interfaces,
# R6 when you must mutate the original object

class and attributes

class is a special attribute used by generics for dispatch. attr adds custom attributes that do not affect dispatch. class<- sets the class directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x <- 1:3
class(x) # "integer"
class(x) <- 'myclass' # set the class directly
x
# List every attribute:
attributes(x)
# Custom attribute:
attr(x, 'note') <- '说明'
attr(x, 'note')
# Test the class:
inherits(x, 'myclass') # TRUE
is.numeric(1:3)
# Order in which a generic searches class:
# class[1] first, then class[2]..., finally default
# Remove the class:
unclass(x)

11.Error handling

stop for errors, warning for warnings, tryCatch for capture, and the condition system.

stop for errors

stop throws an error and halts execution. stopifnot quickly asserts a condition. Error messages should clearly explain the cause.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
divide <- function(a, b) {
if (b == 0) stop('除数不能为 0')
a / b
}
divide(1, 0) # raises an error
# stopifnot:
check <- function(x) {
stopifnot(is.numeric(x), length(x) > 0)
x
}
check('a') # raises an error
# Custom error class:
stop('自定义错误', call. = FALSE)
# Attach a condition object:
# stop(simpleError('msg'))

warning

warning emits a non-fatal notice without interrupting execution. suppressWarnings silences them. options(warn=2) upgrades them to errors.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
warn_if <- function(x) {
if (any(is.na(x))) warning('存在 NA')
x
}
warn_if(c(1, NA))
# Silence it:
suppressWarnings(warn_if(c(NA)))
# Turn every warning into an error:
options(warn = 2)
# Restore:
options(warn = 0)
# Catch warnings as they are signalled:
withCallingHandlers(
expr,
warning = function(w) print('有警告')
)

tryCatch

tryCatch captures errors/warnings and returns the handler's result. It has three parts: error, warning, and finally.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
safe_div <- function(a, b) {
tryCatch(
a / b,
error = function(e) paste('错误:', conditionMessage(e)),
warning = function(w) paste('警告:', conditionMessage(w)),
finally = cat('完成\n')
)
}
safe_div(1, 0) # 错误: 除数不能为 0
safe_div(1, 2) # 0.5
# Handle only errors:
tryCatch(stop('boom'), error = function(e) '捕获')
# Keep both the result and the error:
tryCatch(list(ok = TRUE, val = 1), error = function(e) list(ok = FALSE))

try for fault tolerance

try returns the expression's value, or on failure an object of class try-error, without halting the whole flow. Common in batch processing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
out <- try(log('a'), silent = TRUE)
class(out) # "try-error"
inherits(out, 'try-error') # TRUE
# Fault-tolerant loop:
res <- vector('list', 3)
for (i in 1:3) {
res[[i]] <- try(log(i - 2), silent = TRUE)
}
# Filter out the failures:
failed <- sapply(res, inherits, 'try-error')
# The successes:
res[!failed]
# Supply a default on error:
out2 <- tryCatch(log(-1), error = function(e) NA)
out2

Condition system

R's errors, warnings, and messages are all condition objects. signalCondition raises them; withCallingHandlers catches and continues.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signalCondition(simpleError('出错了'))
# Condition classes:
stopifnot(identical(conditionMessage(simpleError('x')), 'x'))
# Messages:
message('普通提示')
# Capture a message:
withCallingHandlers(
message('hi'),
message = function(m) cat('捕获:', conditionMessage(m), '\n')
)
# A condition carries the call stack:
f <- function() stop('deep')
# Catch it with tryCatch:
tryCatch(f(), error = function(e) conditionCall(e))
# A condition object can carry custom fields:
# simpleError('msg', call = sys.call())

Intercepting warnings

withCallingHandlers catches warnings without halting execution so you can log them and continue. Combine it with invokeRestart.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
log_warnings <- function(expr) {
ws <- list()
res <- withCallingHandlers(
expr,
warning = function(w) {
ws[[length(ws) + 1]] <<- conditionMessage(w)
invokeRestart('muffleWarning')
}
)
list(result = res, warnings = ws)
}
out <- log_warnings({ warning('w1'); 42 })
out$result # 42
out$warnings # "w1"
# Difference from tryCatch:
# withCallingHandlers can resume the original expression afterwards

restarts

An advanced signal-handling mechanism: the caller can provide recovery actions. invokeRestart fires one from the catching side.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Define a restart:
withRestarts(
{ signalCondition(simpleError('x')); '继续' },
my_restart = function() '恢复动作'
)
# Catch it and restart:
withCallingHandlers(
withRestarts(stop('err'), abort = function() 'aborted'),
error = function(e) invokeRestart('abort')
)
# Common restarts:
# muffleWarning, muffleMessage
# Custom: the user picks one inside the error handler
# A good fit for interactive retry logic

Errors inside loops

During batch processing, a single failure should not abort the whole run. tryCatch each item for fault tolerance and keep the surviving results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
items <- list(1, 'a', 3, -1)
process <- function(x) if (x < 0) stop('负数') else x * 2
# Handle it safely:
out <- lapply(items, function(x) {
tryCatch(process(x), error = function(e) NA)
})
unlist(out) # 2 NA 6 NA
# Record why it failed:
out2 <- lapply(items, function(x) {
tryCatch(list(ok = TRUE, v = process(x)),
error = function(e) list(ok = FALSE, msg = conditionMessage(e)))
})
# Keep going:
# purrr::possibly / safely can help

Custom errors

A custom error class carries extra fields. Construct one with simpleError or structure; mark the type with class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
my_error <- function(msg, code = 500) {
structure(
list(message = msg, call = NULL, code = code),
class = c('my_error', 'error', 'condition')
)
}
stop(my_error('余额不足', code = 400))
# Catch it and read the fields:
fail <- function() stop(my_error('boom'))
tryCatch(
fail(),
my_error = function(e) paste('自定义:', e$code),
error = function(e) '其他错误'
)
# Create a condition class:
# condition <- structure(list(), class = c('x', 'condition'))

12.File and data I/O

CSV, readr, line-by-line reading, RDS, JSON, connections, and binary I/O.

CSV read/write

read.csv / write.csv read and write CSV. stringsAsFactors=FALSE prevents conversion to factors. check.names cleans column names.

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(a = 1:3, b = c('x', 'y', 'z'))
write.csv(df, 'out.csv', row.names = FALSE)
# Read it back:
d <- read.csv('out.csv')
# Avoid factors:
read.csv('out.csv', stringsAsFactors = FALSE)
# Custom separator:
write.table(df, 'out.tsv', sep = '\t', row.names = FALSE)
read.delim('out.tsv')
# No header row:
read.csv('f.csv', header = FALSE)
# Specify the encoding:
# read.csv('f.csv', fileEncoding = 'UTF-8')

readr read/write

readr is tidyverse's fast CSV reader. read_csv auto-detects column types; write_csv writes quickly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(readr)
write_csv(df, 'out.csv')
read_csv('out.csv')
# Types are inferred automatically, and it is faster:
read_csv('out.csv', col_types = 'di') # d=double i=integer
# Large datasets:
read_csv('big.csv', show_col_types = FALSE)
# Specify column names:
read_csv('f.csv', col_names = c('x', 'y'))
# Write it back:
write_csv(df, 'out2.csv')
# Others:
# read_tsv / write_tsv for tabs
# read_delim(file, delim = '|')
# See the README for progress and types

Line-by-line reading

readLines reads a file into a character vector, one line per element. writeLines writes it back. Read large files in chunks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
writeLines(c('第一行', '第二行'), 'f.txt')
lines <- readLines('f.txt')
lines # "第一行" "第二行"
# Specify the encoding:
readLines('f.txt', encoding = 'UTF-8')
# Read only the first few lines:
readLines('f.txt', n = 1)
# Chunk through a large file:
con <- file('f.txt', 'r')
while (length(chunk <- readLines(con, n = 10)) > 0) {
cat('块大小', length(chunk), '\n')
}
close(con)
# Write out with an encoding:
writeLines(lines, 'out.txt', useBytes = FALSE)

read.table

read.table is the general table reader; read.csv is a variant of it. Common arguments include header, sep, and na.strings.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Tab separated:
read.table('data.tsv', header = TRUE, sep = '\t')
# No header row:
read.table('f.txt', sep = ',')
# Custom NA values:
read.table('f.csv', header = TRUE, sep = ',', na.strings = c('NA', ''))
# Skip rows:
read.table('f.csv', skip = 2)
# Specify column types:
read.table('f.csv', colClasses = c('numeric', 'character'))
# Row names:
read.table('f.csv', row.names = 1)
# Limit the row count:
read.table('f.csv', nrows = 100)

RDS storage

saveRDS writes a single R object; readRDS restores it. Type and attributes are preserved, which makes it more complete than CSV.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
saveRDS(df, 'df.rds')
d2 <- readRDS('df.rds')
identical(df, d2) # TRUE
# Save several objects:
save(df, x, file = 'all.RData')
load('all.RData') # restores df and x
# Compression:
saveRDS(df, 'df.rds', compress = TRUE)
# Reading from other tools:
# readRDS can read files from earlier versions
# For large data:
# data.table::fread / fwrite are faster
# Inspect the file:
# file.info('df.rds')

JSON read/write

jsonlite is the main JSON package. fromJSON parses, toJSON serializes. Line-delimited JSON handles API responses.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(jsonlite)
json <- '{"name":"R","ver":4.4}'
fromJSON(json) # a named list
# Data frame to JSON:
df <- data.frame(a = 1:2, b = c('x', 'y'))
toJSON(df)
# Nested structure:
fromJSON('{"x":[1,2],"y":{"z":true}}')
# Line-delimited JSON (one object per line):
stream_in(file('rows.jsonl'))
# Array:
fromJSON('[1,2,3]')
# Write to a file:
write(toJSON(df), 'out.json')
# Complex structure to a data frame:
# fromJSON('...', flatten = TRUE)

Connections

A connection abstracts a data source such as a file or a network stream. file opens it; readLines / writeLines read or write; close closes it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
con <- file('f.txt', 'w')
writeLines('一行', con)
close(con)
# Read connection:
con <- file('f.txt', 'r')
content <- readLines(con)
close(con)
# Process it line by line:
con <- file('big.txt', 'r')
while (length(line <- readLines(con, n = 1)) > 0) {
# handle line
}
close(con)
# Text / binary mode:
file('f.bin', 'rb')
# url connection:
url('https://example.com')
# Close automatically:
# on.exit(close(con))

Binary read/write

readBin / writeBin read and write binary data. The raw type holds bytes. Use binary I/O for large files and image data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x <- as.raw(c(0x48, 0x49)) # "H" "I"
writeBin(x, 'f.bin')
back <- readBin('f.bin', 'raw', n = 2)
# Read integers:
writeBin(1:3, 'ints.bin')
readBin('ints.bin', 'integer', n = 3)
# Byte length:
length(x)
# File size:
file.info('f.bin')$size
# Images:
# png::readPNG / jpeg::readJPEG
# Reading / writing large data:
# chunk it with readBin
# Inspect the raw view:
charToRaw('A') # 41

Other formats

readxl reads Excel; haven reads SPSS/Stata; feather/parquet are columnar formats. Load them on demand.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Excel:
# library(readxl)
# read_excel('book.xlsx', sheet = 1)
# readxl::excel_sheets('book.xlsx')
# SPSS / Stata:
# library(haven)
# read_sav('data.sav'); read_dta('data.dta')
# Columnar storage:
# library(arrow)
# write_parquet(df, 'df.parquet')
# read_parquet('df.parquet')
# feather:
# arrow::write_feather(df, 'df.feather')
# Databases:
# library(DBI); dbConnect(RSQLite::SQLite())
# dbReadTable(con, 'tbl')

13.Common pitfalls

Pitfalls R users hit most often, and how to write the code correctly.

Condition length is not 1

if requires a length-1 condition; with a vector it tests only the first element and warns. Summarize with any/all, or vectorize with ifelse.

1
2
3
4
5
6
7
8
x <- c(1, 2, 3)
// BAD if only tests the first element, and warns
if (x == 2) print('找到')
// GOOD use any / all to test the whole vector
if (any(x == 2)) print('找到')
if (all(x > 0)) print('全正')
// GOOD use ifelse for an element-wise condition
ifelse(x == 2, '命中', '未命中')

Factor to numeric

as.numeric on a factor returns the internal integer codes. Convert to character first to get the real values.

1
2
3
4
5
6
7
8
9
f <- factor(c('10', '20', '30'))
// BAD you get the internal codes 1 2 3
as.numeric(f)
// GOOD convert to character first, then to numeric
as.numeric(as.character(f))
// GOOD or index into the levels
as.numeric(levels(f))[f]
# Prevent it when reading a CSV with stringsAsFactors = FALSE
read.csv('f.csv', stringsAsFactors = FALSE)

Dimension drop

Subsetting a matrix to a single row or column drops it to a vector by default. drop = FALSE preserves the dimensions.

1
2
3
4
5
6
7
8
9
10
11
12
m <- matrix(1:9, nrow = 3)
m[1, ] # the vector 1 4 7
// BAD the dimension is lost when you bind rows
rbind(m[1, ], m[2, ])
// GOOD keep the row dimension
rbind(m[1, , drop = FALSE], m[2, , drop = FALSE])
# Keep a matrix after subsetting:
m[1:2, , drop = FALSE]
# Taking one data frame column returns a vector:
df <- data.frame(a = 1:3, b = 4:6)
df[1] # still a data.frame
df[[1]] # a vector

Growing a vector in a loop

c() inside a loop copies the whole vector every iteration, which is extremely slow. Pre-allocate, or vectorize.

1
2
3
4
5
6
7
8
9
10
11
n <- 5000
// BAD every c() copies, O(n^2)
res <- numeric()
for (i in 1:n) res <- c(res, i)
// GOOD preallocate the length
res <- numeric(n)
for (i in 1:n) res[i] <- i
// GOOD vectorise directly (fastest)
res <- 1:n
# Collect a list, then join it:
# do.call(c, lapply(1:n, function(i) i))

NA and comparisons

An NA in a comparison yields NA, never FALSE. Filter NA out with is.na.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x <- c(1, NA, 3)
x == NA # NA NA NA (never do this)
// BAD comparing before filtering NA out
x[x == 1] # NA leaks through
// GOOD exclude NA first
x[!is.na(x) & x == 1]
// GOOD which ignores NA for you
which(x == 1)
# Test for any NA:
any(is.na(x))
# NA and NaN differ:
is.nan(NaN); is.na(NaN) # TRUE TRUE
# Skip them when summarising:
sum(x, na.rm = TRUE)

Partial matching

$ and [[ perform unique-prefix matching. This silent abbreviation of names hides bugs; use full names, or use dplyr.

1
2
3
4
5
6
7
8
9
10
11
12
13
df <- data.frame(age_group = 1:3, score = 4:6)
// BAD silently matches age_group
df$age_g
// GOOD use the full column name
df$age_group
// GOOD [[ matches exactly
df[['age_group']]
# Turning partial matching off:
df[['age_g', exact = FALSE]] # still matches loosely
# dplyr uses tidyselect, which is not fuzzy:
# dplyr::select(df, age_g) # raises an error instead
# List the column names:
names(df)

Vector recycling

Shorter vectors are recycled to the length of the longer one in operations. When the lengths are not a multiple, you only get a warning — bugs hide easily.

1
2
3
4
5
6
7
8
9
10
11
x <- c(1, 2, 3, 4)
y <- c(10, 20)
// BAD recycling: y is padded to c(10, 20, 10, 20)
x + y # 11 22 13 24
// It only warns when the lengths are not multiples:
x + c(1, 2) # length 4 vs 2, no warning
x + c(1, 2, 3) # warning: 3 does not divide 4
// GOOD assert the lengths match
stopifnot(length(x) == length(y))
# Recycling is just as sneaky in comparisons:
# x > c(1, 100)

List flattening

c() tries to flatten one level of lists. Wrap elements in list() to keep them as a list. unlist recurses and can bite you.

1
2
3
4
5
6
7
8
9
10
11
12
13
l <- list(a = list(x = 1), b = list(y = 2))
// BAD c() flattened the top-level list
c(l$a, l$b)
// GOOD keep it nested
list(l$a, l$b)
// GOOD when you do want to flatten, say so
unlist(l, recursive = FALSE)
# Join two lists:
c(list(a = 1), list(b = 2))
# Inspect the structure:
str(c(l$a, l$b))
# Collect elements into a vector:
# do.call(list, l)

Package masking

library-loaded packages mask functions with the same name; later entries in the search path hide earlier ones. Disambiguate with ::.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Both packages export filter:
# library(stats)
# library(dplyr) # dplyr masks stats::filter
// BAD once masked, filter drifts with the load order
filter(x, rep(1, 3)) # might be dplyr::filter
// GOOD call it through an explicit namespace
stats::filter(x, rep(1, 3))
dplyr::filter(df, a > 1)
# See what is masked:
# find('filter')
# Detach a package:
# detach('package:dplyr')
# Import a single function:
# dplyr::select
# Detect conflicts with the conflicted package:
# library(conflicted); conflict_prefer('filter', 'dplyr')

14.Parallel and concurrent

parallel, mclapply, foreach, and future: R's multiprocess parallelism toolkit.

parallel overview

R's parallelism is multiprocess (fork) or socket-based. The parallel package provides mclapply and clusters. There is startup overhead, so tasks need to be large enough to pay for it.

1
2
3
4
5
6
7
8
9
10
library(parallel)
detectCores() # CPU core count
detectCores(logical = FALSE) # physical cores
# Compare serial and parallel timings:
f <- function(i) sqrt(i)
system.time(lapply(1:1e5, f))
# Parallel version:
# system.time(mclapply(1:1e5, f))
# Tiny tasks get slower in parallel (process startup cost)
# Good fit: slow functions, large batch processing

mclapply

mclapply is parallel's parallel lapply, implemented with fork. Available only on Unix (Windows needs a cluster).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(parallel)
# Unix/macOS:
f <- function(i) {
Sys.sleep(0.01); i^2
}
# res <- mclapply(1:8, f, mc.cores = 4)
# On Windows use mc.cores = 1 or a cluster
res <- lapply(1:8, f) # serial fallback
# mc.preschedule: batch scheduling
# mclapply(x, f, mc.cores = 2, mc.preschedule = TRUE)
# Same return type as lapply:
unlist(res)
# Check for failures:
# is.atomic(res)

Cluster parallelism

makeCluster creates a process cluster; parLapply applies a function in parallel. Works on Windows too. Call stopCluster when done.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(parallel)
cl <- makeCluster(2) # 2 workers
# Each worker has its own environment, so send the data:
clusterExport(cl, 'shared_data')
res <- parLapply(cl, 1:4, function(i) i * 2)
stopCluster(cl)
unlist(res)
# Load a package on every worker:
# clusterEvalQ(cl, library(dplyr))
# Preset variables:
# clusterExport(cl, varlist = c('x', 'y'))
# Random seed:
# clusterSetRNGStream(cl)
# On Windows a cluster is the only way to parallelise

foreach

foreach collects results across iterations; %dopar% runs in parallel (needs doParallel). %do% is the sequential variant.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(foreach)
# library(doParallel)
# registerDoParallel(cores = 2)
res <- foreach(i = 1:4) %do% {
i * 2 # serial
}
# Parallel version:
# res <- foreach(i = 1:4, .combine = c) %dopar% i * 2
# Combine the results:
# .combine = c / rbind / list
# Pass packages and dependencies:
# .packages = 'dplyr', .export = 'fun'
# Stop:
# stopImplicitCluster()
# Merge the results:
unlist(res)

future

The future package models parallelism as a "future value." future() launches an async task; value() retrieves it. plan chooses the strategy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# library(future)
plan(multisession) # multisession strategy
f <- future({
Sys.sleep(0.1)
42
}) # returns a future immediately
value(f) # 42, blocks for the result
# In bulk:
# library(furrr)
# plan(multisession)
# future_map(1:4, ~ .x * 2)
# Switch strategy:
# plan(sequential) # back to serial
# Error propagation:
# try(value(future(stop('x'))))
# Globals are shipped automatically:
# the x in future({ x + 1 }) is carried over

Shared state

Parallel workers do not share memory: each copies its own environment. Guard file writes against concurrent conflicts, and collect results via return values.

1
2
3
4
5
6
7
8
9
10
11
12
# Data must be handed to the workers explicitly:
shared <- 1:10
# mclapply(shared, function(x) x * 2)
# use clusterExport, or let future capture it
# Concurrent writes to one file collide:
# have each worker write its own file:
# file <- paste0('out_', i, '.csv')
# Collect and aggregate:
res <- parLapply(cl, 1:4, function(i) i^2)
final <- Reduce(`+`, res)
# Globals are invisible inside a worker:
# export them, or you get an object-not-found error

Random seeds

Each worker needs its own seed in parallel runs. Combine set.seed globally with clusterSetRNGStream (or future.seed) to keep results reproducible.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
set.seed(42)
rnorm(3) # reproducible when serial
# Seeding a parallel cluster:
# clusterSetRNGStream(cl)
# or:
# clusterSetRNGStream(cl, 123)
# Seeding parallel foreach:
# registerDoParallel(); set.seed(1)
# future manages it for you:
# future.seed = TRUE
# Verify reproducibility:
set.seed(7)
a <- rnorm(5)
set.seed(7)
b <- rnorm(5)
identical(a, b) # TRUE

Parallel performance

Gains are limited by task granularity, core count, and communication overhead. Profile the serial bottleneck first, then parallelize the biggest chunk.

1
2
3
4
5
6
7
8
9
10
11
12
# Do not parallelise when the overhead dwarfs the task
f <- function(i) sqrt(i)
system.time(lapply(1:1e5, f))
# system.time(mclapply(1:1e5, f, mc.cores = 4))
# Good fit: each task takes tens of ms or more
# Watch how it scales:
# time(1 core) vs time(4 cores)
# Communication bottleneck: copying big data around is slow
# Advice: return results as small objects
# Avoid shipping large shared objects repeatedly
# Timing tools:
# microbenchmark::microbenchmark(...)

15.HTTP requests

Downloads, httr2/httr requests, JSON APIs, URL handling, and web scraping.

Downloading files

download.file fetches a file. mode controls binary mode. R.utils supports resuming partial downloads. Set timeout on slow links.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
download.file(
'https://example.com/data.csv',
'data.csv',
mode = 'wb'
)
# Set a timeout:
# download.file(url, dest, timeout = 60)
# Check the result:
file.info('data.csv')$size
# Read what was downloaded:
read.csv('data.csv')
# Binary data:
# download.file(url, 'img.png', mode = 'wb')
# Mirror related:
# setInternet2(TRUE) # old Windows

httr2 requests

httr2 is the next-generation HTTP client. req_perform sends a request; resp_body_json parses the response. Build requests in a pipeline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(httr2)
req <- request('https://api.example.com')
req <- req %>%
req_url_path_append('v1') |>
req_url_query(q = 'rlang') |>
req_headers('Authorization' = 'Bearer xx')
# resp <- req_perform(req)
# resp_body_json(resp)
# Error handling:
# tryCatch(req_perform(req), error = function(e) ...)
# Rate limiting:
# req_throttle(req, rate = 10)
# Authentication:
# req_auth_bearer_token(req, 'token')

GET Requests

GET reads a resource. Base R's readLines can pull plain text; httr's GET plus content parses the response. Query parameters are appended to the URL.

1
2
3
4
5
6
7
8
9
10
11
12
13
# A simple GET with base R:
txt <- readLines('https://example.com')
# library(httr)
resp <- httr::GET('https://api.example.com/items')
httr::status_code(resp) # 200
httr::content(resp, 'text')
# With query parameters:
httr::GET('https://api.example.com/search',
query = list(q = 'rlang', page = 2))
# Response headers:
httr::headers(resp)
# Connection timeout:
httr::GET(url, httr::timeout(30))

POST Requests

POST submits data. The body carries JSON or form fields. Two styles are common: httr POST and httr2 req_body_json.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# library(httr)
httr::POST(
'https://api.example.com/login',
body = list(user = 'a', pass = 'b'),
encode = 'form'
)
# JSON body:
httr::POST(
'https://api.example.com/submit',
body = '{"x":1}',
content_type_json()
)
# httr2 style:
# req_body_json(req, list(x = 1))
# Parse the response:
# resp <- httr::POST(...)
# httr::content(resp, 'parsed')
# Upload a file:
# httr::POST(url, body = list(f = upload_file('f.csv')))

JSON APIs

Calling a JSON API means a request plus jsonlite parsing. The result is often a nested list, so flatten it into a data frame. Mind pagination and error handling.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(httr)
# library(jsonlite)
url <- 'https://api.github.com/repos/tidyverse/dplyr'
resp <- httr::GET(url)
stopifnot(httr::status_code(resp) == 200)
js <- httr::content(resp, 'text')
info <- jsonlite::fromJSON(js)
info$full_name
info$stargazers_count
# List to data frame:
# fromJSON(js, flatten = TRUE)
# Pagination:
# loop over the page / per_page query parameters
# Uniform error handling:
# tryCatch(..., error = ...)

URL Handling

parse_url splits a URL into components and URLencode escapes it. URLdecode reverses the encoding. Base R covers this out of the box.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(httr)
u <- parse_url('https://user:[email protected]/a?b=1&c=2')
u$scheme; u$hostname; u$path
u$query # b=1 c=2
# Modify it, then rebuild:
u$query$d <- 3
build_url(u)
# URL encoding:
URLencode('a b/c') # a%20b%2Fc
URLdecode('%E4%BD%A0%E5%A5%BD')
# Encoding non-ASCII text:
URLencode('你好')
# Resolve a relative path:
# url_absolute('/a', 'https://x.com')
# Parse the query string:
# parse_url('https://x.com/?a=1')$query

Web Scraping

rvest parses HTML. html_elements selects nodes and html_text extracts the text. Respect robots rules and rate limits.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(rvest)
url <- 'https://example.com'
page <- read_html(url)
page %>% html_elements('h1') %>% html_text()
page %>% html_elements('a') %>%
html_attr('href')
# Tables:
# page %>% html_table()
# CSS selectors:
page %>% html_elements('#main p')
page %>% html_elements('.item')
# XPath:
page %>% html_elements(xpath = '//div[@class="x"]')
# Respect the site's rules:
# rate limits, identify yourself with a User-Agent
# Lawful use: analysing public data

TCP Sockets

socketConnection opens a TCP connection for reading and writing strings. It suits simple protocols and internal services.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
con <- socketConnection(
host = 'localhost',
port = 9999,
server = FALSE, # client side
open = 'r+b'
)
# Write:
writeLines('hello', con)
# Read:
line <- readLines(con, n = 1)
close(con)
# Start a server:
# srv <- socketConnection(host = 'localhost',
# port = 9999, server = TRUE)
# Note: it blocks while waiting
# For complex protocols prefer httpuv / plumber
# For production endpoints use an API framework, not a raw socket

16.Time and Dates

Sys.Date / POSIXct, formatting, lubridate and time zones.

Current Time

Sys.Date gives today's date and Sys.time the current date-time. date returns the current time as a string.

1
2
3
4
5
6
7
8
9
10
11
12
13
Sys.Date() # 2026-08-02
Sys.time() # POSIXct date-time
format(Sys.time())
date() # "Sat Aug ... 2026"
# Take the components:
as.POSIXlt(Sys.time())
unclass(as.POSIXlt(Sys.time()))
# Timestamp:
unclass(Sys.time()) # seconds
# Time zone:
Sys.timezone()
# Set it manually:
# Sys.setenv(TZ = 'Asia/Shanghai')

Date Basics

The Date class is R's date type. as.Date converts strings, and you can add or subtract days. unclass reveals the underlying day count.

1
2
3
4
5
6
7
8
9
10
11
12
13
d <- as.Date('2026-08-02')
class(d) # "Date"
d + 1 # tomorrow
Sys.Date() - d # difference in days
difftime(Sys.Date(), d, units = 'days')
# Parse other formats:
as.Date('2026/08/02')
as.Date('02-08-2026', format = '%d-%m-%Y')
# Sequence:
seq(as.Date('2026-01-01'), by = 'day', length.out = 3)
# Components:
format(d, '%Y-%m-%d')
format(d, '%A') # day of the week

POSIXct

POSIXct stores a second-level timestamp, while POSIXlt breaks it into components. as.POSIXct parses strings and handles time zones.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
t <- as.POSIXct('2026-08-02 10:30:00')
class(t) # POSIXct POSIXt
unclass(t) # seconds since 1970
# Specify the time zone:
as.POSIXct('2026-08-02 10:30:00', tz = 'UTC')
# Convert to components:
lt <- as.POSIXlt(t)
lt$year + 1900 # 2026
lt$mon + 1 # month 8
lt$mday; lt$hour; lt$min
# Format as a string:
format(t, '%Y-%m-%d %H:%M:%S')
# Arithmetic:
t + 3600 # +1 hour
# Convert to and from Date:
as.Date(t)

Formatting

strftime / format output according to placeholders. %Y year, %m month, %d day, %H hour, %M minute, %S second.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
d <- Sys.Date()
t <- Sys.time()
format(d, '%Y-%m-%d')
format(d, '%d/%m/%Y')
format(d, '%A') # weekday name
format(t, '%H:%M:%S')
format(t, '%Y 年第 %j 天')
# Parsing:
as.Date('02/08/2026', format = '%d/%m/%Y')
# strptime:
strptime('2026-08-02 10:00', format = '%Y-%m-%d %H:%M')
# Placeholder cheat sheet:
# %Y 4-digit year %y 2-digit year %m month %d day
# %H %M %S hour/minute/second %A weekday name
# %j day of the year

lubridate Overview

lubridate offers friendly date functions: ymd / ymd_hms for parsing, year / month for components, and interval arithmetic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# library(lubridate)
ymd('20260802')
ymd('2026-08-02')
ymd_hms('2026-08-02 10:30:00')
# Take the components:
year(t); month(t); day(t)
hour(t); minute(t); second(t)
wday(t) # weekday (as a number)
# Arithmetic:
t + days(1); t - weeks(2)
months(3) # 3 months
# Intervals:
interval(d, Sys.Date()) |> as.duration()
# Time zone:
with_tz(t, 'UTC')
# Rounding:
round_date(t, 'hour')
floor_date(t, 'day')

Date Sequences

seq generates date sequences with by set to day, month or year. Use seq.Date for arbitrary intervals and business days.

1
2
3
4
5
6
7
8
9
10
11
seq(as.Date('2026-01-01'), as.Date('2026-01-10'), by = 'day')
seq(as.Date('2026-01-01'), by = 'month', length.out = 3)
seq(as.Date('2026-01-01'), by = 'year', length.out = 2)
# Fix the count instead:
seq(as.Date('2026-01-01'), as.Date('2026-12-31'), length.out = 5)
# seq.Date is more explicit:
seq.Date(as.Date('2026-01-01'), by = '2 days', length.out = 3)
# Working days:
# filter out the weekend:
x <- seq(as.Date('2026-01-01'), by = 'day', length.out = 30)
x[!weekdays(x) %in% c('Saturday', 'Sunday')]

Time Differences

Subtracting dates yields a difftime. units sets the unit and as.numeric extracts the number. Handy for timing code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
t0 <- Sys.time()
Sys.sleep(0.5)
Sys.time() - t0 # Time difference
# Specify the units:
difftime(Sys.time(), t0, units = 'secs')
difftime(Sys.time(), t0, units = 'mins')
# Get a plain number:
as.numeric(Sys.time() - t0)
# Date difference:
Sys.Date() - as.Date('2026-01-01')
# Average duration:
# mean works on difftime
# Custom units:
# difftime(t2, t1, units = 'hours')
# Timing tools:
# system.time(expr)
# tictoc::tic() / toc()

Time Zones

The tz argument sets the time zone. Sys.timezone reports the current one and OlsonNames lists the valid names. Use them to compare instants across zones.

1
2
3
4
5
6
7
8
9
10
11
12
Sys.timezone() # current time zone
OlsonNames() # every valid time zone name
# Parse in a given time zone:
as.POSIXct('2026-08-02 10:00', tz = 'Asia/Shanghai')
# One instant shown in different zones:
t <- as.POSIXct('2026-08-02 10:00', tz = 'UTC')
format(t, tz = 'Asia/Shanghai')
format(t, tz = 'America/New_York')
# Convert between zones:
with_tz(t, 'Asia/Shanghai') # lubridate
# UTC timestamp:
# as.integer(as.POSIXct('2026-08-02', tz = 'UTC'))

17.Processes and Environment

Running system commands, environment variables, command-line arguments, platform information and paths.

Running Commands

system runs a system command and returns its exit code. system2 passes arguments more safely. Capture output with capture or intern.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
system('echo hello')
system2('echo', 'hello')
# Capture the output:
system('date', intern = TRUE)
system2('date', stdout = TRUE)
# Exit code:
code <- system('ls')
code # 0 means success
# Pass arguments separately to avoid shell injection:
# system2('cp', c('a.txt', 'b.txt'))
# Discard the output:
system('echo x', ignore.stdout = TRUE)
# Timeout:
# system2('cmd', timeout = 10)

Environment Variables

Sys.getenv reads and Sys.setenv writes. unsetenv removes a variable. PATH and R-related variables come up most often.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sys.getenv('PATH')
Sys.setenv(MY_VAR = 'hello')
Sys.getenv('MY_VAR') # "hello"
Sys.unsetenv('MY_VAR')
# List them all:
Sys.getenv()
# With a default:
Sys.getenv('NOPE', unset = 'default')
# Common variables:
Sys.getenv('HOME')
Sys.getenv('R_VERSION') # or:
R.version.string
# Conditional check:
if (nzchar(Sys.getenv('CI'))) print('在 CI 环境')

Command-line arguments

commandArgs retrieves script arguments. trailingOnly strips R's own arguments. Parse them with optparse or by hand in base R.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Run: Rscript app.R --name Rex
args <- commandArgs(trailingOnly = TRUE)
args # "--name" "Rex"
# Quick parsing:
get_arg <- function(name) {
i <- which(args == name)
if (length(i)) args[i + 1] else NULL
}
get_arg('--name')
# Proper parsing:
# library(optparse)
# parser <- OptionParser(option_list = list(
# make_option('--name', type = 'character')))
# opts <- parse_args(parser)
# Defaults:
# Check the argument count:
stopifnot(length(args) >= 1)

Exit Status

quit exits R. q('no') skips saving the workspace. Use quit(status = 1) to exit with an error so scripts can react.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
quit(save = 'no') # quit without saving
# Set a status code:
# quit(status = 1) # exit as a failure
# Conditional exit:
if (!file.exists('data.csv')) {
message('缺文件')
quit(status = 1)
}
# Ending a script normally:
# an Rscript just ends at its last line
# Read the exit code (shell):
# Rscript app.R; echo $?
# Pause instead of exiting with browser:
# browser() # pause for debugging

Platform Information

R.version gives the version, Sys.info the system details and .Platform the platform specifics. Useful for cross-platform concerns such as path separators.

1
2
3
4
5
6
7
8
9
10
11
12
13
R.version.string # R 4.4.x
version # full version details
Sys.info() # list of system information
Sys.info()['sysname'] # Windows/Linux/Darwin
.Platform$file.sep # / or \
.Platform$OS.type # "windows" / "unix"
# Conditional check:
if (.Platform$OS.type == 'windows') 'Win' else 'Unix'
# Architecture:
R.version$arch
# Path separator:
# file.path handles it for you:
file.path('a', 'b', 'c.txt') # correct separator per platform

Path Management

file.path joins paths, dirname / basename split them, and normalizePath produces a canonical absolute path. file.exists tests for existence.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
file.path('data', 'sub', 'f.csv')
basename('a/b/f.csv') # "f.csv"
dirname('a/b/f.csv') # "a/b"
file.exists('f.csv')
dir.exists('data')
# Working directory:
getwd()
setwd('data') # change it with care
# Normalise a path:
normalizePath('..')
# Expand ~:
path.expand('~/R')
# List a directory:
list.files('.')
list.files('.', pattern = '\\.csv$')
# Create a directory:
dir.create('out', showWarnings = FALSE)

Sleeping and Waiting

Sys.sleep pauses for a given number of seconds. Use it to rate-limit or to wait for an external resource. The unit is seconds.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Sys.sleep(1) # pause for 1 second
t0 <- Sys.time()
Sys.sleep(0.5)
Sys.time() - t0
# Rate-limit a loop:
for (i in 1:3) {
cat(i, '\n')
Sys.sleep(0.2) # once every 0.2s
}
# Wait for a file to appear:
# while (!file.exists('done.txt')) Sys.sleep(1)
# Wait on a network resource:
# pair it with an httr timeout
# Note: it blocks the current process

Rscript Scripts

Rscript is the non-interactive entry point, ideal for scheduled jobs and batch processing. A shebang can go at the top of the script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Top of the script (Unix):
# #!/usr/bin/env Rscript
# Run:
# $ Rscript script.R arg1
# Usage:
args <- commandArgs(trailingOnly = TRUE)
message('开始处理: ', args[1])
# Emit a structured result:
cat('RESULT:', mean(1:100), '\n')
# Exit code:
# quit(status = 1) on failure
# Batch pipelines:
# read stdin:
input <- readLines(file('stdin'), n = 1)
# write stdout and the pipe carries on

18.Regular Expressions

grep / grepl, gsub substitution, regexpr positions, stringr and common patterns.

grep and grepl

grep returns matching indices while grepl returns a logical vector. ignore.case makes matching case-insensitive. value returns the matched values.

1
2
3
4
5
6
7
8
9
10
11
12
13
x <- c('apple', 'banana', 'apricot')
grepl('^ap', x) # TRUE FALSE TRUE
grep('^ap', x) # 1 3
grep('^ap', x, value = TRUE) # "apple" "apricot"
grep('^AP', x, ignore.case = TRUE)
# Fixed matching:
grepl('a.', x, fixed = TRUE) # a literal .
# Counting:
sum(grepl('a', x))
# Inverting:
x[!grepl('^ap', x)]
# Several patterns:
grepl('a|b', c('x', 'a'))

gsub Substitution

gsub replaces every match, sub only the first. \\1 refers to a capture group. perl=TRUE enables the extended syntax.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gsub('a', 'o', 'banana') # "bonono"
sub('a', 'o', 'banana') # "bonana"
# Delete the matches:
gsub('[0-9]', '', 'a1b2')
# Capture group backreferences:
gsub('(\\d{4})-(\\d{2})', '\\2/\\1', '2026-08')
# Several alternatives:
gsub('a|b', 'x', 'abacus')
# Ignore case:
gsub('a', 'o', 'ABC', ignore.case = TRUE)
# Fixed literal:
gsub('a.b', 'x', 'a.b', fixed = TRUE)
# Vectorised:
gsub('s', 'S', c('sun', 'sea'))

regexpr Positions

regexpr returns the position and length of the first match, gregexpr all of them. regmatches extracts the matched text.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s <- 'price is 99 dollars'
m <- regexpr('[0-9]+', s)
m # position and length
regmatches(s, m) # "99"
# All matches:
ms <- gregexpr('[a-z]+', 'a1bc2def')
regmatches('a1bc2def', ms) # a list
# Extract matches and captures:
# unlist after matching:
unlist(regmatches('x12y34', gregexpr('[0-9]+', 'x12y34')))
# When nothing matches:
regexpr('z', 'abc') # -1
# Using match positions as indices:
# combine it with substring

Syntax Basics

Core regex metacharacters: ^ start, $ end, . any, . character classes [], groups (), quantifiers * + ? {}, escaping \\.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'^abc' # starts with abc
'abc$' # ends with abc
'^[a-z]+$' # all lowercase letters
'[0-9]{2,4}' # 2 to 4 digits
'colou?r' # colour/color
'a.c' # a?c, any single character
'\\.' # a literal dot
'[^0-9]' # not a digit
'(ab|cd)' # group with alternation
'\\d' '\\w' '\\s' # digit/word char/whitespace
# Backslashes must be doubled in R:
# the regex \d is written \\d in an R string
# Verify:
grepl('^[0-9]+$', '123') # TRUE

Common Patterns

A quick reference of common patterns: email, phone, date, whitespace cleanup and number extraction. Tweak them to fit your needs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Extract digits:
regmatches('v1.2.3', regexpr('[0-9]+', 'v1.2.3'))
# Strip whitespace:
gsub('[[:space:]]', '', 'a b c')
# A rough email pattern:
pat <- '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$'
grepl(pat, '[email protected]') # TRUE
# Date YYYY-MM-DD:
pat <- '^\\d{4}-\\d{2}-\\d{2}$'
grepl(pat, '2026-08-02')
# CJK characters:
grepl('[\\u4e00-\\u9fa5]', '你好')
# Extract what is inside parentheses:
sub('.*\\((.+)\\).*', '\\1', 'name(value)')
# Splitting:
strsplit('a,b;c', '[,;]')

stringr Matching

stringr keeps the same regex syntax but a consistent interface. str_detect / str_extract / str_match plus their _all variants.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# library(stringr)
str_detect(c('a1', 'b2'), '\\d')
str_extract('price 99', '\\d+')
str_extract_all('a1 b22', '\\d+')
str_match('a=1;b=2', 'a=(\\d+)')
str_match_all('a1b2', '([ab])(\\d)')
# Positions:
str_locate('abcabc', 'b')
str_locate_all('abcabc', 'b')
# Replacement:
str_replace('a-b', '-', '_')
str_replace_all('a-b-c', '-', '+')
# Build the regex explicitly:
str_detect('apple', regex('^ap'))

stringr Substitution

str_replace / str_replace_all perform replacement. The pattern can use fixed or perl. str_remove deletes a match.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# library(stringr)
str_replace('one-two', '-', '_')
str_replace_all('a-b-c', '-', '')
# Backreferences:
str_replace('2026-08', '(\\d{4})-(\\d{2})', '\\2/\\1')
# Removal:
str_remove('xx-abc', 'xx-')
str_remove_all('a1b2', '\\d')
# Fixed literal:
str_replace_all('a.b', fixed('.'), 'X')
# Case insensitive:
str_replace('ABC', 'a', 'x', regex(ignore_case = TRUE))
# Vectorised automatically:
str_replace_all(c('a1', 'b2'), '\\d', '#')
# Anchored match:
str_replace('abc', '^a', 'X')

Flags and Extensions

perl=TRUE enables PCRE extensions such as lookahead and named groups. fixed=TRUE matches literally. ignore.case is also available.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Lookahead:
grepl('a(?=b)', 'ab', perl = TRUE) # TRUE
# Negative lookahead:
grepl('a(?!b)', 'ac', perl = TRUE) # TRUE
# Named groups:
# gsub('(?<y>\\d+)', '\\k<y>', s, perl = TRUE)
# Non-greedy:
regmatches('a<b>c<b>', regexpr('<.+?>', 'a<b>c<b>', perl = TRUE))
# Multiline / dot matches newline:
# grepl('a.b', 'a\nb', perl = TRUE)
# fixed literal:
grepl('a.b', 'a.b', fixed = TRUE)
# Combining every flag:
# grepl(pat, x, ignore.case = TRUE, perl = TRUE)

19.Packages and Builds

CRAN installation, renv dependency management, package structure and R CMD, testthat, roxygen.

Installing Packages

install.packages installs from CRAN. update.packages upgrades. library loads a package. Use devtools / remotes for development versions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
install.packages('dplyr')
# Install from a mirror:
install.packages('ggplot2', repos = 'https://cloud.r-project.org')
# Update:
# update.packages()
# Development version:
# remotes::install_github('tidyverse/dplyr')
# Local file:
# install.packages('path/pkg_0.1.0.tar.gz', repos = NULL)
# See what is installed:
rownames(installed.packages())
# Load:
library(dplyr)
# Install dependencies:
# install.packages(c('dplyr', 'tidyr'))

CRAN and Repositories

CRAN is the official package repository. repos selects a mirror. available.packages lists what can be installed. CRAN policy governs releases.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Current repository:
getOption('repos')
# Every available package:
ap <- available.packages()
ap[1:5, 'Package']
# Search for a package:
# grep the fields of available.packages()
# Package information:
# packageDescription('dplyr')
# Dependencies:
# packageDescription('dplyr')$Depends
# CRAN checks:
# it must pass R CMD check before release
# Mirror list:
# https://cran.r-project.org/mirrors.html
# China mirror:
# https://mirrors.tuna.tsinghua.edu.cn/CRAN/

renv Dependency Management

renv pins project dependency versions, much like Python's venv. renv::init sets it up, and snapshot / restore sync the lockfile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Initialise:
# renv::init()
# Install a dependency:
# renv::install('dplyr')
# Record the dependencies:
# renv::snapshot() # writes renv.lock
# Restore on another machine:
# renv::restore()
# Check the status:
# renv::status()
# Isolate the project library:
# renv::activate()
# Upgrade:
# renv::update()
# Things to watch:
# .Rprofile loads renv automatically
# commit renv.lock to version control

Package Structure

The standard R package layout: DESCRIPTION, R/, man/, tests/, NAMESPACE. R/ holds the source and man/ the documentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# mypkg/
# ├── DESCRIPTION metadata: name/version/deps
# ├── NAMESPACE which functions are exported
# ├── R/ source files *.R
# ├── man/ documentation *.Rd
# ├── tests/ tests
# └── data/ bundled data
# Key DESCRIPTION fields:
# Package: mypkg
# Version: 0.1.0
# Imports: dplyr
# Generate a skeleton with usethis:
# usethis::create_package('mypkg')
# Documentation:
# usethis::use_roxygen_md()

R CMD Commands

The R CMD commands build and check packages: build packs, check validates, INSTALL installs. check is mandatory before release.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Build the tar.gz:
# R CMD build mypkg
# Check it (the crucial step):
# R CMD check mypkg_0.1.0.tar.gz
# Install:
# R CMD INSTALL mypkg
# Common check options:
# --as-cran mimics CRAN's strict checks
# Equivalent at runtime:
# system('R CMD INSTALL --help')
# From inside an R session:
# system2('Rscript', c('-e', '1+1'))
# The devtools wrapper:
# devtools::check()

testthat Testing

testthat is the mainstream testing framework. expect_equal and friends are the assertions, and test_that organises cases. usethis generates the test files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# library(testthat)
f <- function(x) x * 2
test_that('f 正确加倍', {
expect_equal(f(2), 4)
expect_identical(f(0), 0)
expect_true(f(-1) < 0)
expect_error(f('a'))
})
# Run:
# testthat::test_dir('tests')
# or while developing:
# devtools::test()
# Common assertions:
# expect_equal / expect_identical
# expect_true / expect_false
# expect_warning / expect_message
# Wire it into CI to run automatically

Formatting and Linting

styler enforces a uniform code style and lintr does static checking. Pair them with RStudio Addins or pre-commit to stay tidy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# library(styler)
# Format one file:
# styler::style_file('R/foo.R')
# Format the whole package:
# styler::style_pkg()
# Preview the style diff:
# styler::style_file('R/foo.R', dry = 'only')
# Lint with lintr:
# lintr::lint('R/foo.R')
# Common configuration:
# a .lintr file selects the linters
# Run it automatically:
# pre-commit hooks
# CI integration:
# lintr as a check step

roxygen Documentation

roxygen2 generates .Rd documentation from comments. Lines start with #' and use @param / @return / @export tags. The docs live next to the source.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#' Add two numbers
#'
#' @param a the first number
#' @param b the second number
#' @return the sum
#' @export
add2 <- function(a, b) a + b
# Generate the docs:
# devtools::document()
# or:
# roxygen2::roxygenise()
# Generate the NAMESPACE exports:
# @export writes into NAMESPACE for you
# Package-level docs:
# @keywords internal
# Check the docs:
# R CMD check

Reproducible Sessions

sessionInfo records versions and dependencies for reproducibility. renv.lock pins the dependencies. Record the R version too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sessionInfo() # R version + loaded packages
R.version.string
# Sample output:
# R version 4.4.x
# Platform: ...
# attached packages and their versions
# Keys to reproducibility:
# 1. record the sessionInfo() output
# 2. renv::snapshot() to lock dependencies
# 3. pin the R version (Docker image)
# Check what is missing:
# Dependency list:
# renv::dependencies()
# When reporting an issue, attach:
# sessionInfo() + a minimal reproducible example

Official Links

Direct links to the official docs and resources.

About this Cheatsheet

This page is a self-contained cheatsheet for R 4.4, covering roughly 80% of the base R and tidyverse scenarios you meet in real data analysis. It leans toward modern idioms: the native pipe `|>`, the anonymous-function shorthand `\(x)`, `data.frame` with `stringsAsFactors = FALSE`, vectorized `ifelse`, typed collection operations via `purrr::map_*`, and R's distinctive copy-on-modify semantics alongside environment reference semantics. For authoritative material, see the official R manuals and R for Data Science. Nineteen chapters each focus on one topic — from your first program, variables and types through to the apply family, S3/R6 object orientation, parallelism and networking. Every chapter is split into 8–9 worked sections of 5–20 lines each, roughly 160 topics in total. The snippets are deliberately short and self-explanatory, ready to paste straight into R or RStudio. Everything runs in your browser — no uploads, no tracking. This page is part of the GuruToolkit collection of free developer tools; the snippets are free to use, with no warranty.

Version 2.1.0