R purrr - map() Function with Examples

The purrr package revolutionizes functional programming in R by providing a consistent, predictable interface for iteration. While base R's `lapply()` works, `map()` offers superior error handling,...

Key Insights

  • The map() family in purrr provides type-safe iteration with consistent syntax, replacing base R’s apply functions with predictable output types and better error messages
  • map() always returns a list, while variants like map_dbl(), map_chr(), and map_lgl() guarantee specific atomic vector outputs or fail explicitly
  • Advanced patterns include mapping with multiple inputs using map2() and pmap(), extracting nested elements with shortcuts, and composing functions for complex transformations

Why purrr::map() Over Base R

The purrr package revolutionizes functional programming in R by providing a consistent, predictable interface for iteration. While base R’s lapply() works, map() offers superior error handling, type safety, and readability.

library(purrr)

# Base R approach
numbers <- list(1:5, 6:10, 11:15)
lapply(numbers, mean)

# purrr approach - identical output, clearer intent
map(numbers, mean)

The real advantage emerges with type-specific variants:

# Returns numeric vector, not list
map_dbl(numbers, mean)
# [1]  3.0  8.0 13.0

# Fails fast if output isn't numeric
tryCatch(
  map_dbl(list("a", "b"), toupper),
  error = function(e) message("Type mismatch caught!")
)

Basic map() Syntax and Anonymous Functions

The map() function takes a vector or list as input and applies a function to each element. You can pass functions three ways:

data <- list(
  a = c(1, 2, 3, NA, 5),
  b = c(10, 20, 30, 40, 50),
  c = c(100, NA, 300, 400, 500)
)

# Named function
map(data, mean, na.rm = TRUE)

# Anonymous function (formula syntax)
map(data, ~ mean(.x, na.rm = TRUE))

# Anonymous function (traditional)
map(data, function(x) mean(x, na.rm = TRUE))

The formula syntax (~) is purrr’s shorthand where .x represents the current element. For simple operations, this dramatically reduces boilerplate.

Type-Safe Variants for Atomic Outputs

Each map_*() variant enforces output types, preventing silent failures:

# Numeric outputs
map_dbl(1:5, ~ .x^2)
# [1]  1  4  9 16 25

# Character outputs
map_chr(1:3, ~ paste("Item", .x))
# [1] "Item 1" "Item 2" "Item 3"

# Logical outputs
map_lgl(list(1, "text", NULL), is.numeric)
# [1]  TRUE FALSE FALSE

# Data frame outputs (row-bind results)
map_dfr(1:3, ~ data.frame(id = .x, value = .x * 10))
#   id value
# 1  1    10
# 2  2    20
# 3  3    30

Extracting Elements with Shortcuts

Purrr provides powerful shortcuts for extracting nested elements without writing custom functions:

users <- list(
  list(name = "Alice", age = 30, scores = c(85, 90, 88)),
  list(name = "Bob", age = 25, scores = c(78, 82, 80)),
  list(name = "Carol", age = 35, scores = c(92, 95, 93))
)

# Extract by name
map_chr(users, "name")
# [1] "Alice" "Bob"   "Carol"

# Extract by position
map_dbl(users, 2)
# [1] 30 25 35

# Extract nested elements
map(users, list("scores", 1))
# [[1]] [1] 85
# [[2]] [1] 78
# [[3]] [1] 92

# Safe extraction with default values
map_dbl(users, "salary", .default = NA)
# [1] NA NA NA

Working with Data Frames

The map() family excels at column-wise operations on data frames:

library(dplyr)

df <- data.frame(
  x = 1:100,
  y = rnorm(100),
  z = runif(100)
)

# Apply function to each column
map_dbl(df, mean)
#          x          y          z 
# 50.5000000  0.1234567  0.5123456

# Get column types
map_chr(df, class)
#         x         y         z 
# "integer" "numeric" "numeric"

# Create summary statistics
map_dfr(df, ~ data.frame(
  mean = mean(.x),
  sd = sd(.x),
  min = min(.x),
  max = max(.x)
), .id = "variable")

Multiple Input Mapping with map2() and pmap()

When you need to iterate over multiple inputs simultaneously:

# map2() for two inputs
x <- list(1, 10, 100)
y <- list(2, 20, 200)

map2_dbl(x, y, ~ .x + .y)
# [1]   3  30 300

# Weighted mean example
values <- list(c(1, 2, 3), c(10, 20, 30), c(100, 200, 300))
weights <- list(c(1, 1, 1), c(2, 1, 2), c(3, 2, 1))

map2_dbl(values, weights, ~ weighted.mean(.x, .y))
# [1]   2  20 150

# pmap() for multiple inputs (takes list of arguments)
params <- list(
  mean = c(0, 5, 10),
  sd = c(1, 2, 3),
  n = c(5, 5, 5)
)

pmap(params, rnorm)

Handling Errors Gracefully

Purrr provides safely(), possibly(), and quietly() for robust error handling:

# safely() returns list with result and error
safe_log <- safely(log)

map(list(10, -5, "text"), safe_log)
# [[1]]
# $result: [1] 2.302585
# $error: NULL
# 
# [[2]]
# $result: NULL
# $error: <simpleError: NaNs produced>

# possibly() returns default value on error
poss_log <- possibly(log, otherwise = NA)

map_dbl(list(10, -5, "text"), poss_log)
# [1] 2.302585       NaN       NA

# Extract successful results
results <- map(list(10, -5, 100), safe_log)
map_dbl(results, "result", .default = NA)

Advanced Pattern: Nested Mapping

Combine map() calls for complex nested operations:

# Simulate multiple experiments with multiple trials
experiments <- list(
  exp1 = list(trial1 = rnorm(10), trial2 = rnorm(10)),
  exp2 = list(trial1 = rnorm(10), trial2 = rnorm(10))
)

# Calculate mean for each trial in each experiment
map(experiments, ~ map_dbl(.x, mean))

# Flatten nested structure
map(experiments, ~ map_dbl(.x, mean)) %>%
  map_dfr(~ data.frame(trial1 = .x[1], trial2 = .x[2]), .id = "experiment")

Performance Considerations

While map() prioritizes clarity over raw speed, it performs competitively:

# For large datasets, consider furrr for parallel processing
library(furrr)
plan(multisession, workers = 4)

large_list <- replicate(1000, rnorm(1000), simplify = FALSE)

# Sequential
system.time(map_dbl(large_list, mean))

# Parallel
system.time(future_map_dbl(large_list, mean))

The purrr package transforms R’s functional programming capabilities with its consistent, type-safe interface. The map() family eliminates common iteration pitfalls through explicit type contracts, superior error messages, and elegant syntax for complex transformations. Whether extracting nested elements, handling errors gracefully, or processing data frames column-wise, map() provides the foundation for readable, maintainable R code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.