rguides

Reduce

Reduce takes a binary function and a vector or list, then applies the function cumulatively: given [a, b, c] and binary function f, it computes f(f(a, b), c). The final result is a single value of the same type that f returns.

Signature

Reduce(f, x, init, right = FALSE, accumulate = FALSE)

Parameters:

  • f, a binary function (takes exactly two arguments)
  • x, a vector or list to reduce
  • init, optional initial value for the accumulator
  • right, if TRUE, fold right-to-left instead of left-to-right
  • accumulate, if TRUE, return all intermediate results instead of just the final one

Basic usage

Sum and product

Reduce(`+`, c(1, 2, 3, 4, 5))
# [1] 15

Reduce(`*`, c(1, 2, 3, 4, 5))
# [1] 120

Set intersection is another operation that naturally cascades through Reduce: intersect(a, b) returns the common elements of two sets, and Reduce extends this to find elements shared across an arbitrary number of sets by applying intersect pairwise. This pattern generalises to any binary set operation — union for the full collection of elements across all sets, or setdiff for successive removal. The approach is the idiomatic way to handle collections of sets in base R without writing explicit loops.

Set intersection

Reduce(intersect, list(c("a", "b", "c"), c("b", "c", "d"), c("c", "d", "e")))
# [1] "c"

The init parameter supplies a starting value for the accumulator, which matters most when the input vector could be empty. Without init, an empty vector causes Reduce to error because there is no first element to serve as the initial accumulator. With init, the function returns the initial value unchanged, making your reduction safe for edge cases where the input has length zero. This is the same pattern used in functional languages like Haskell and OCaml, where folds always take an explicit starting value.

Initial value

Pass init to set a starting value for the reduction:

Reduce(`+`, c(1, 2, 3), init = 10)
# [1] 16

Reduce(`+`, c(), init = 10)
# [1] 10

Without init, an empty vector causes an error because Reduce has no value to begin the accumulation with. With init, empty vectors return the initial value safely — the function body is never evaluated. This guard matters in pipelines where upstream steps might produce empty results: rather than wrapping every Reduce call in a conditional, you supply a sensible default. For numeric reductions, init = 0 works for addition and init = 1 works for multiplication, matching the mathematical identity element of each operation.

Right folding

Set right = TRUE to fold from right to left:

Reduce(`-`, c(10, 3, 2))
# [1] 5

Reduce(`-`, c(10, 3, 2), right = TRUE)
# [1] 9

Left fold: ((10 - 3) - 2) = 5 Right fold: 10 - (3 - 2) = 9

Setting accumulate = TRUE changes the return value from a single scalar to a vector of all intermediate results. This is the same concept as cumsum or cumprod, but generalised to any binary function you supply. Each element in the output corresponds to the reduced value after processing the input up to that position. The first element is always the first element of the input itself (or init, if supplied), and the last element matches what accumulate = FALSE would have returned. This mode is particularly useful for debugging reduction logic or tracking how a value evolves step by step.

Accumulate mode

Set accumulate = TRUE to return all intermediate results:

Reduce(`+`, c(1, 2, 3, 4, 5), accumulate = TRUE)
# [1]  1  3  6 10 15

Running totals are the simplest use of accumulate mode, but the pattern extends to any operation where you want to see how a value changes across the sequence. Running minima, maxima, or products are all one-liners: just swap the binary function. For character vectors, Reduce(paste0, x, accumulate = TRUE) builds up progressively longer strings. This approach replaces manual for-loop accumulation with a declarative one-liner that reads closer to the intent of the computation.

# Running minimum
Reduce(pmin, c(5, 3, 4, 1, 2), accumulate = TRUE)
# [1] 5 3 3 1 1

Beyond numeric reductions, Reduce can compose structured objects where each step builds on the previous result. This is the functional-programming pattern of folding a list of transformations or configuration pieces into a single composite value. The combining function receives the accumulated value so far and the next element, and returns the updated accumulator. For merging configuration lists, data frames, or nested environments, Reduce removes the boilerplate of a manual accumulation loop and keeps the logic in one place — the binary function.

Building complex objects

Reduce composes operations where later steps depend on earlier results:

# Build a configuration list from partial pieces
configs <- list(
  list(theme = "light"),
  list(font = "Arial"),
  list(size = 14)
)

combined <- Reduce(function(cfg, new) c(cfg, new), configs, init = list())
# [[1]]$theme  [1] "light"
# [[2]]$font  [1] "Arial"
# [[3]]$size  [1] 14

Common pitfalls

Reduce is strict about its expectations: the binary function must accept exactly two arguments and return a single value compatible with the accumulator type. The most frequent mistakes come from passing a unary function, expecting Reduce to broadcast it element-wise (use lapply for that), or forgetting that the return type of the combining function determines the type of every subsequent step.

Function must be binary

Reduce passes exactly two arguments at a time. Supplying a function that takes only one argument triggers deeply nested evaluation errors because each successive call wraps the previous result in another layer of the function:

Reduce(function(x) x^2, c(1, 2, 3))
# Error: evaluation nested too deeply

To use a unary function inside Reduce, wrap it in a binary adapter that takes two arguments — the accumulator and the current element — and applies the unary function to the current element before combining. This pattern is common when the reduction logic involves transforming each element and then accumulating the transformed values, as opposed to applying the transform after the reduction is complete:

Reduce(function(a, b) a + b^2, c(1, 2, 3))
# [1] 32

The second common pitfall involves the output shape: with accumulate = TRUE, the return value is a vector or list of length equal to the input, not a scalar. Code that assumes a single return value will break when accumulate is active, producing length mismatches in downstream assignments or comparisons. Always check the accumulate parameter when debugging unexpected output lengths from Reduce calls.

Mistaking accumulated value

With accumulate = TRUE, the output length changes:

result <- Reduce(`+`, c(1, 2, 3), accumulate = TRUE)
length(result)
# [1] 3

When to use reduce vs alternatives

Reduce is the right tool when each step depends on the result of the previous step and you cannot parallelize or vectorize the operation. For independent per-element operations, use lapply or sapply instead. For cumulative totals of numbers, cumsum, cumprod, cummax, and cummin are considerably faster because they are implemented in C without the overhead of R function calls. Use Reduce when your combining function is not one of those built-in primitives, merging data frames, intersecting sets across a list, or building up a configuration object step by step.

Reduce() applies a binary function cumulatively to a list. With accumulate = TRUE it returns all intermediate results, like a running total. It is the base R equivalent of purrr’s reduce(). A common pattern is Reduce("+", list_of_vectors) to sum a list of same-length vectors element-wise, or Reduce(merge, list_of_dataframes) to join multiple data frames on a shared key column.

Merging multiple data frames on a shared key

# Three data frames sharing a common ID column
customers <- data.frame(id = 1:4, name = c("Alice", "Bob", "Charlie", "Diana"))
orders <- data.frame(id = c(1, 2, 2, 3, 4), product = c("Widget", "Gadget", "Thing", "Widget", "Doohickey"))
payments <- data.frame(id = c(1, 3, 4), amount = c(99.50, 45.00, 120.75))

# Merge all three in one step
all_data <- Reduce(function(x, y) merge(x, y, by = "id", all = TRUE),
                   list(customers, orders, payments))

all_data
#   id    name   product amount
# 1  1   Alice    Widget  99.50
# 2  2     Bob    Gadget     NA
# 3  2     Bob     Thing     NA
# 4  3 Charlie    Widget  45.00
# 5  4   Diana Doohickey 120.75

Reduce with merge() is the base R equivalent of a chain of left joins, combining an arbitrary number of data frames on a shared key. Each step merges the accumulated result with the next data frame in the list, and all = TRUE preserves rows that appear in any table. Bob appears without a payment amount because he has no matching entry in the payments frame — exactly the behaviour you would expect from a full outer join. For production code with many tables, the tidyverse purrr::reduce(list(df1, df2, df3), dplyr::full_join, by = "id") offers the same pattern with clearer syntax.

See also