purrr::reduce
Overview
reduce() takes a binary function (a function that takes two arguments) and applies it cumulatively to a list or vector. Given [a, b, c] and a function f, it computes f(f(a, b), c). This is called a “fold” or “accumulation” in functional programming.
It’s useful when you have a list of things that need to be combined into one value — merging data frames, building up a string, composing functions, or chaining operations.
Basic Usage
library(purrr)
# Sum numbers by folding addition
nums <- c(1, 2, 3, 4, 5)
reduce(nums, `+`)
#> [1] 15
# That's equivalent to ((((1 + 2) + 3) + 4) + 5)
The function + is passed infix-style using the backtick syntax. You can also use a lambda:
reduce(c(1, 2, 3, 4, 5), \(acc, x) acc + x)
#> [1] 15
Providing an Initial Value
Use .init to start with a seed value:
# Start from 10, then add each element
reduce(c(1, 2, 3), `+`, .init = 10)
#> [1] 16
When .init is provided, the first call is f(.init, first_element) instead of f(first_element, second_element).
Chaining with Pipes
reduce() fits naturally in a %>% pipeline:
library(dplyr)
list(
tibble(x = 1:3, y = c("a", "b", "c")),
tibble(x = 4:6, y = c("d", "e", "f")),
tibble(x = 7:9, y = c("g", "h", "i"))
) |>
reduce(bind_rows)
#> # A tibble: 9 x 2
#> x y
#> <int> <chr>
#> 1 1 a
#> 2 2 b
#> 3 3 c
#> 4 4 d
#> 5 5 e
#> 6 6 f
#> 7 7 g
#> 8 8 h
#> 9 9 i
Here bind_rows() takes two data frames and combines them. reduce() applies it repeatedly until all data frames are merged.
merge()ing Named Lists
configs <- list(
list(host = "localhost", port = 5432),
list(user = "alice", password = "secret"),
list(db = "production")
)
reduce(configs, function(acc, x) {
modifyList(acc, x)
})
#> $host
#> [1] "localhost"
#>
#> $port
#> [1] 5432
#>
#> $user
#> [1] "alice"
#>
#> $password
#> [1] "secret"
#>
#> $db
#> [1] "production"
modifyList() recursively merges two lists, so reduce() merges all of them together.
Composing Functions
A functional programming classic — build up a composed function from right to left:
f <- function(x) x + 1
g <- function(x) x * 2
h <- function(x) x - 3
# ((x - 3) * 2) + 1 — note the order is right to left
pipeline <- reduce(list(f, g, h), compose)
pipeline(10)
#> [1] ((10 - 3) * 2) + 1 = 15
reduce() with compose() builds the function composition. Note that compose() applies right-to-left, so the last function in the list is applied first.
reduce() vs for loops
reduce() replaces accumulation loops with a functional style:
# Traditional for loop
result <- 1
for (x in c(2, 3, 4, 5)) {
result <- result * x
}
# reduce() version
result <- reduce(c(2, 3, 4, 5), `*`, .init = 1)
The reduce() version makes the accumulation intent explicit and avoids mutable state.
reduce_right() — Fold from the Right
reduce_right() folds from the last element to the first. Given [a, b, c] it computes f(a, f(b, c)):
# reduce from left: (((1 - 2) - 3) - 4)
reduce(c(1, 2, 3, 4), `-`)
#> [1] -8
# reduce from right: (1 - (2 - (3 - 4)))
reduce_right(c(1, 2, 3, 4), `-`)
#> [1] -2
For commutative operations like + and *, the result is the same. For non-commutative operations like string concatenation or matrix multiplication, the direction matters.
accumulate() — See Intermediate Results
accumulate() does the same as reduce() but returns all intermediate results:
accumulate(c(1, 2, 3, 4, 5), `+`)
#> [1] 1 3 6 10 15
Compare to reduce() which only returns the final value:
reduce(c(1, 2, 3, 4, 5), `+`)
#> [1] 15
accumulate() is useful when you want to inspect or plot the progression of the accumulation.
reduce2() and accumulate2() — Three-Argument Functions
reduce2() and accumulate2() pass three arguments to the function: the accumulator, the current element, and an extra vector:
# For operations that need an additional parameter
files <- c("a.csv", "b.csv", "c.csv")
reduce2(
files,
function(acc, file, sep) {
if (is.null(acc)) {
readLines(file)
} else {
c(acc, readLines(file))
}
},
.init = NULL
)
This pattern is useful when the combining function needs extra data that isn’t in the list being reduced.
Short-Circuiting with done()
From purrr 1.0, you can terminate an accumulation early by returning done(value) from the reducing function:
library(purrr)
# Find the first value that exceeds a threshold
values <- c(1, 2, 5, 3, 8, 12, 15)
accumulate(values, function(acc, x) {
if (x > 10) done(x) else x
})
#> [1] 1 2 5 3 8 12
The accumulation stops at the first done(), returning that value and discarding the rest.
Empty Input
When the input is empty, reduce() errors by default. Provide .init to handle this case:
reduce(integer(), `+`)
#> Error: `x` must have at least 1 record
reduce(integer(), `+`, .init = 0)
#> [1] 0
Common Use Cases
Merging lists or data frames — reduce(list_of_dfs, full_join) combines multiple tables by a common key.
Building a character string — reduce(vec, paste0) concatenates strings.
Chaining transformations — reduce(list(fn1, fn2, fn3), compose) builds a processing pipeline.
Set operations — reduce(list_of_sets, union) finds the union of many sets.
See Also
- /reference/tidyverse/purrr-map/ — apply a function to each element and collect results
- /reference/tidyverse/purrr_pmap/ — reduce over multiple inputs in parallel
- /guides/purrr-functional-programming/ — functional programming patterns with purrr including reduce