循环的核心,是对一组输入重复执行同一个操作。除了显式编写 for,R 还可以把“要执行的函数”和“要处理的数据”分别交给迭代函数。
purrr 是 tidyverse 中负责函数式编程的包。它提供了一组命名一致、返回类型明确的工具,尤其适合处理向量、列表和多组参数。
map():对每个元素调用函数
map() 对输入的每个元素应用同一个函数,并始终返回 list。
library(purrr)
x <- list(1, 2, 3, 4)
map(x, sqrt)
它与 base R 的 lapply() 作用相近:
lapply(x, sqrt)
map(x, sqrt)
当后续代码需要确定的原子向量时,使用带类型后缀的版本:
map_lgl(c(-1, 0, 1), ~ .x > 0)
map_int(1:4, ~ .x^2)
map_dbl(x, sqrt)
map_chr(letters[1:5], toupper)
| 函数 | 返回值 |
|---|---|
map() |
list |
map_lgl() |
logical vector |
map_int() |
integer vector |
map_dbl() |
double vector |
map_chr() |
character vector |
类型化版本不会随结果内容猜测返回类型。如果某次结果不能转换成指定类型,它会直接报错,而不是悄悄改变整个对象的结构。
编写匿名函数
公式写法中的 .x 代表当前元素:
map_dbl(1:5, ~ .x^2)
它等价于:
map_dbl(1:5, function(x) x^2)
当函数已有合适的参数形式时,可以直接传函数名:
map_dbl(1:5, sqrt)
需要向函数传递额外参数时,可以写在迭代函数后面:
map_dbl(1:5, round, digits = 2)
从 list 中提取元素
map() 的 .f 也可以是名称或位置,用于从一组结构相同的对象中提取字段。
records <- list(
list(id = 1, name = "Alice"),
list(id = 2, name = "Bob"),
list(id = 3, name = "Carol")
)
map_chr(records, "name")
map_int(records, "id")
这种写法适合模型结果、API 返回值和嵌套 list,不需要为一次字段提取单独定义函数。
map2():两个输入同步迭代
map2() 同时接收两个等长输入,每次把同一位置的两个元素传给函数。
x <- 1:5
y <- 6:10
map2_dbl(x, y, ~ .x + .y)
公式中的 .x 和 .y 分别代表第一个与第二个输入。
files <- c("a.csv", "b.csv", "c.csv")
groups <- c("control", "treated", "validation")
labels <- map2_chr(files, groups, ~ paste(.y, .x, sep = ": "))
labels
这里的同步映射不是多进程并行计算。map2() 仍然依次处理元素;真正的并行执行属于 future、furrr 等工具的范围。
pmap():多个输入同步迭代
输入超过两个时,将参数组织成 list 再交给 pmap():
params <- list(
a = 1:3,
b = 4:6,
c = 7:9
)
pmap_dbl(params, function(a, b, c) a + b + c)
每次迭代会从 a、b 和 c 的同一位置各取一个值。
这适合批量调用带多组参数的函数:
simulations <- list(
n = c(100, 500, 1000),
mean = c(0, 5, 10),
sd = c(1, 2, 3)
)
samples <- pmap(
simulations,
function(n, mean, sd) rnorm(n, mean, sd)
)
reduce():把多个值逐步合成一个
reduce() 不保留每次迭代的独立结果,而是把前一步结果与下一个元素继续合并。
reduce(1:5, `+`)
计算过程相当于:
((((1 + 2) + 3) + 4) + 5)
合并多个向量:
sets <- list(
c("A", "B", "C"),
c("B", "C", "D"),
c("C", "D", "E")
)
reduce(sets, intersect)
合并多个数据框时,也可以把连接函数作为归约操作:
tables <- list(table_a, table_b, table_c)
result <- reduce(tables, dplyr::left_join, by = "id")
accumulate():保留每一步结果
accumulate() 与 reduce() 使用同样的逐步合并逻辑,但会保存中间状态。
accumulate(1:5, `+`)
# 1 3 6 10 15
需要观察累计值、逐步更新状态或调试归约过程时,accumulate() 比只返回最终结果的 reduce() 更合适。
安全调用
批量处理时,一个输入报错可能使整次迭代中断。purrr 提供包装函数,把错误、警告或输出变成可以继续处理的结果。
safely()
safely() 返回包含 result 和 error 的 list:
safe_log <- safely(log)
safe_log(10)
safe_log("a")
批量运行后,可以分别提取成功结果和错误:
results <- map(list(1, 10, "a"), safe_log)
map(results, "result")
map(results, "error")
possibly()
只关心结果时,可以为失败调用设置默认值:
possible_log <- possibly(log, otherwise = NA_real_)
map_dbl(list(1, 10, "a"), possible_log)
quietly()
需要同时捕获返回值、标准输出、消息和警告时,使用 quietly():
quiet_summary <- quietly(summary)
result <- quiet_summary(1:10)
result$result
result$output
result$warnings
result$messages
这些工具适合批量任务的结果收集,但不应被用来隐藏所有错误。默认值必须能明确表示失败,否则后续分析可能把失败结果当成真实数据。
转换嵌套 list 的方向
当一组结果都是结构相同的 list 时,transpose() 可以把“按记录组织”转换为“按字段组织”。
records <- list(
list(a = 1, b = 2),
list(a = 3, b = 4)
)
transpose(records)
结果相当于:
list(
a = list(1, 3),
b = list(2, 4)
)
这在整理 safely() 产生的结果、模型输出或 API 响应时很有用。
与 base R 的对应关系
| base R | purrr | 主要结果 |
|---|---|---|
lapply(x, f) |
map(x, f) |
list |
sapply(x, f) |
map_*() |
指定类型的向量 |
mapply(f, x, y) |
map2(x, y, f) |
双输入映射 |
Map(f, ...) |
pmap(list(...), f) |
多输入映射 |
Reduce(f, x) |
reduce(x, f) |
一个归约结果 |
Filter(f, x) |
keep(x, f) |
保留满足条件的元素 |
purrr 不是让循环消失,而是把循环模式命名出来:
- 每个输入产生一个结果:
map(); - 两个或多个输入同步变化:
map2()、pmap(); - 所有输入合成一个结果:
reduce(); - 批量任务需要保留错误:
safely()、possibly()。
选择函数时,先确定输入有几组、每次输出是什么类型,以及最终要保留每次结果还是只保留归约结果。