06使用 purrr 进行函数式迭代

用 map、map2、pmap、reduce 和安全调用工具完成单输入、多输入、归约及容错迭代。

2026-02-28
RpurrrIterationFunctional Programming
本章目录 · 13

循环的核心,是对一组输入重复执行同一个操作。除了显式编写 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() 仍然依次处理元素;真正的并行执行属于 futurefurrr 等工具的范围。

pmap():多个输入同步迭代

输入超过两个时,将参数组织成 list 再交给 pmap()

params <- list(
  a = 1:3,
  b = 4:6,
  c = 7:9
)

pmap_dbl(params, function(a, b, c) a + b + c)

每次迭代会从 abc 的同一位置各取一个值。

这适合批量调用带多组参数的函数:

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() 返回包含 resulterror 的 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()

选择函数时,先确定输入有几组、每次输出是什么类型,以及最终要保留每次结果还是只保留归约结果。