Functional Programming with purrr

· 6 min read · Updated March 10, 2026 · intermediate
purrr functional-programming iteration tidyverse

Functional programming (FP) transforms how you write R code. Instead of writing loops that mutate objects, you create small functions that transform data and compose them together. The purrr package, part of the tidyverse, provides a consistent toolkit for functional programming in R.

This guide covers the core purrr functions, shows how they compare to base R alternatives, and demonstrates real-world patterns that will make your code more readable and maintainable.

Why Use purrr?

Base R provides the apply family (apply(), lapply(), sapply(), tapply()), but these functions have inconsistent APIs and return types. purrr offers:

  • Consistent syntax — every function follows the same pattern
  • Type safetymap() variants guarantee output types (integer, character, data frame)
  • Error handling — built-in wrappers for graceful failure management
  • Composition — functions work seamlessly with other tidyverse tools

The map() Family

The core of purrr is map(), which applies a function to each element of a vector or list.

library(purrr)

# Apply a function to each element
numbers <- list(1:3, 4:6, 7:9)
map(numbers, sum)
# [[1]]
# [1] 6
# [[2]]
# [1] 15
# [[3]]
# [1] 24

Type-Specific map() Variants

purrr provides type-specific variants that return specific data types, eliminating the need for type conversion:

# map_chr returns character vector
names <- list(c("apple", "banana"), c("cherry", "date"))
map_chr(names, ~ paste(.x, collapse = "-"))
# [1] "apple-banana" "cherry-date"

# map_dbl returns numeric vector
map_dbl(list(1:3, 4:6, 7:9), mean)
# [1]  2  5  8

# map_int returns integer vector
map_int(list(c(1, 2), c(3, 4)), length)
# [1] 2 2

# map_lgl returns logical vector
map_lgl(list(1, 0, 1), ~ .x > 0)
# [1] TRUE FALSE TRUE

# map_df returns data frame
map_df(list(mtcars[1:3, ], mtcars[4:6, ]), nrow)
# # A tibble: 1 × 1
#     nrow
#    <int>
# 1      6

Anonymous Functions and Shortcuts

Use the tilde syntax for inline anonymous functions:

# Full anonymous function
map(numbers, function(x) x * 2)

# Tilde shortcut (purrr style)
map(numbers, ~ .x * 2)

# Named component extraction
people <- list(
  list(name = "Alice", age = 30),
  list(name = "Bob", age = 25),
  list(name = "Carol", age = 35)
)
map_chr(people, ~ .x$name)
# [1] "Alice" "Bob"   "Carol"

map_int(people, ~ .x$age)
# [1] 30 25 35

Working with Multiple Arguments: map2() and pmap()

When you need to iterate over two or more vectors in parallel, use map2() for two arguments or pmap() for multiple arguments:

# map2: iterate over two vectors
x <- c(10, 20, 30)
y <- c(1, 2, 3)

map2_dbl(x, y, ~ .x / .y)
# [1] 10 10 10

# pmap: iterate over multiple vectors
df <- data.frame(
  a = c(1, 2, 3),
  b = c(10, 20, 30),
  c = c(100, 200, 300)
)

pmap_dbl(df, ~ ..1 + ..2 * ..3)
# [1] 1011 2022 3033

The ..1, ..2, etc. syntax in pmap refers to positional arguments from the data frame or list.

Handling Errors: safely() and possibly()

Production code needs graceful error handling. purrr provides wrappers that transform errors into manageable outputs:

# safely: returns list with result and error
safe_sqrt <- safely(sqrt, otherwise = NA)

results <- map(list(4, 9, -1, 16), safe_sqrt)
str(results)
# List of 4
#  $ :List of 2
#  ..$ result: num 2
#  ..$ error : NULL
#  $ :List of 2
#  ..$ result: num 3
#  ..$ error : NULL
#  $ :List of 2
#  ..$ result: num NA
#  ..$ error :List of 2
#  .. ..$ message: "NaN" ...
#  $ :List of 2
#  ..$ result: num 4
#  ..$ error : NULL

# Extract successful results
map_dbl(results, "result")
# [1]  2  3 NA  4

# possibly: simpler, returns default on error
possibly_sqrt <- possibly(sqrt, default = NA_real_)

map_dbl(list(4, 9, -1, 16), possibly_sqrt)
# [1]  2  3 NA  4

Walking: walk() for Side Effects

Use walk() when you want to perform side effects (printing, saving files, sending emails) without caring about the return value:

# Print each element (side effect)
walk(list("a", "b", "c"), ~ cat(.x, "\n"))

# Save multiple plots to files
plots <- list(
  ggplot(mtcars, aes(mpg, wt)) + geom_point(),
  ggplot(mtcars, aes(cyl, mpg)) + geom_boxplot()
)

walk2(plots, c("scatter.png", "box.png"), ~ ggsave(.x, filename = .y))

# Create multiple directories
walk(c("output/figures", "output/data", "output/reports"), 
     ~ dir.create(.x, recursive = TRUE, showWarnings = FALSE))

Reducing and Accumulating

reduce() and accumulate() collapse a list into a single value by repeatedly applying a binary function:

# reduce: accumulate to single value
numbers <- list(c(1, 2), c(3, 4), c(5, 6))

reduce(numbers, c)
# [1] 1 2 3 4 5 6

reduce(list(1, 2, 3, 4), `+`)
# [1] 10

# accumulate: show all intermediate results
accumulate(list(1, 2, 3, 4), `+`)
# [1]  1  3  6 10

# Practical example: join multiple data frames
df1 <- data.frame(id = 1:2, x = c("a", "b"))
df2 <- data.frame(id = 2:3, y = c("c", "d"))
df3 <- data.frame(id = 3:4, z = c("e", "f"))

reduce(list(df1, df2, df3), dplyr::left_join, by = "id")
#   id x    y    z
# 1  1 a <NA> <NA>
# 2  2 b    c <NA>
# 3  3 <NA> d    e
# 4  4 <NA> <NA> f

Filtering and Selecting: keep() and discard()

Extract elements based on conditions without explicit loops:

x <- list(1, 2, 3, 4, 5, 6)

keep(x, ~ .x %% 2 == 0)
# [[1]]
# [1] 2
# [[2]]
# [1] 4
# [[3]]
# [1] 6

discard(x, ~ .x %% 2 == 0)
# [[1]]
# [1] 1
# [[2]]
# [1] 3
# [[3]]
# [1] 5

# compact: remove NULL and empty elements
y <- list(1, NULL, 2, character(0), 3, list())
compact(y)
# [[1]]
# [1] 1
# [[2]]
# [1] 2
# [[3]]
# [1] 3

purrr vs Base R

Here’s a practical comparison showing why purrr is often preferred:

# Base R approach
lapply(mtcars, class)

# purrr approach
map(mtcars, class)

# Base R: sapply attempts to simplify (unreliable)
sapply(mtcars, mean)
# Returns named numeric vector, but behavior varies

# purrr: explicit type
map_dbl(mtcars, mean)  # Fails if not numeric
map_chr(mtcars, class)  # Always returns character

# Base R: error handling is manual
result <- tryCatch(lapply(1:3, function(x) sqrt(x)),
                   error = function(e) NA)

# purrr: built-in error handling
map(1:3, safely(sqrt))

When to Use purrr

Use purrr when you need:

  • Consistent, predictable behavior across different data types
  • Type-safe iteration that fails loudly on type mismatches
  • Error handling without verbose tryCatch blocks
  • Readable code that clearly expresses intent

Stick with base R when:

  • Working in environments without tidyverse
  • Performance is critical (base R can be faster for simple operations)
  • Existing codebase uses apply functions

The purrr package transforms iterative code into elegant, functional expressions. Start replacing your loops with map functions, and your R code will become more concise and maintainable.