Functional Programming with purrr
Prerequisites: The apply family
purrr is the tidyverse's toolkit for applying functions to lists and vectors. Its functions are consistent and predictable about what they return — which the apply family isn't always.
map and its typed variants
library(purrr)
words <- c("apple", "banana", "kiwi")
map(words, nchar)
map_int(words, nchar)
map_chr(words, toupper)
map_dbl(1:3, \(x) x / 2)
[[1]]
[1] 5
[[2]]
[1] 6
[[3]]
[1] 4
[1] 5 6 4
[1] "APPLE" "BANANA" "KIWI"
[1] 0.5 1.0 1.5
map()always returns a list.map_int(),map_dbl(),map_chr(), andmap_lgl()return a vector of that type — or give a clear error if a result doesn't fit.
Iterating over two inputs, and filtering
library(purrr)
map2_dbl(c(10, 20, 30), c(0.1, 0.2, 0.3), \(price, tax) price * (1 + tax))
numbers <- list(3, "x", 7, NULL, 12)
keep(numbers, is.numeric) |> unlist()
[1] 11 24 39
[1] 3 7 12
keep() and discard() filter a list by a condition; reduce() combines
elements into one value.
A real use: many files, one table
A very common task — read every CSV file in a folder and stack them:
files <- list.files("data/monthly", pattern = "\\.csv$", full.names = TRUE)
all_sales <- files |>
purrr::map(readr::read_csv) |>
purrr::list_rbind()
Handling failures
safely() wraps a function so errors are captured instead of stopping
the whole loop:
library(purrr)
safe_log <- safely(log)
results <- map(list(10, "a", 100), safe_log)
map(results, "result") |> map_dbl(\(x) if (is.null(x)) NA else x) |> round(2)
[1] 2.30 NA 4.61
Each result holds either a result or an error, so one bad input
doesn't lose all the others.
Common mistakes
- Using
map()when you wanted a vector, then struggling with a list. Use the typedmap_*()variant.