mapply()
mapply(FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE) mapply() applies a function to multiple arguments simultaneously, processing the first element of each argument together, then the second elements, and so on. It is the multivariate counterpart of sapply(), where lapply() loops over a single structure and sapply() loops and simplifies, mapply() handles parallel iteration across multiple inputs.
The function recycles shorter arguments to match the length of the longest, making it useful for operations that need to combine corresponding elements from different vectors or lists.
Syntax
mapply(FUN, ..., MoreArgs = NULL, SIMPLIFY = TRUE, USE.NAMES = TRUE)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
FUN | function | , | Function to apply, resolved via match.fun() |
... | vectors or lists | , | Arguments to iterate over in parallel; recycled to the longest length |
MoreArgs | list | NULL | Additional arguments passed to FUN on every call |
SIMPLIFY | logical or character | TRUE | Attempt to reduce the result to a vector, matrix, or higher-dimensional array |
USE.NAMES | logical | TRUE | Use the names of the first ... argument as result names |
Return value
When SIMPLIFY = FALSE, mapply() always returns a list with one element per iteration. When SIMPLIFY = TRUE (the default), it attempts to simplify the result:
- If all results are scalar numbers, returns a numeric vector
- If all results have the same length, returns a matrix or array
- Otherwise, returns a list
Examples
Repeating values with parallel arguments
The classic example is using mapply() with rep(), where the first argument provides values and the second provides repetition counts:
mapply(rep, 1:4, 4:1)
# [[1]]
# [1] 1 1 1 1
#
# [[2]]
# [1] 2 2 2
#
# [[3]]
# [1] 3 3
#
# [[4]]
# [1] 4
This is equivalent to calling rep(1, times = 4), rep(2, times = 3), rep(3, times = 2), and rep(4, times = 1) in sequence, but done in one call. When you pass positional arguments to the ... of mapply(), they get forwarded to each invocation of FUN in the order they appear. For functions like rep() that accept both positional and named arguments, switching to named arguments inside the ... gives you explicit control over which parameter receives which vector, which is especially valuable when the function signature is non-obvious or has many optional parameters.
Named arguments passed to FUN
When you name the arguments in ..., those names become argument names in the function call. This can make code more readable:
mapply(rep, times = 1:4, x = 4:1)
# Same result as above, but rep() receives named arguments
Swapping between positional and named ... arguments is a common refactoring step as scripts grow more complex. When you start with two vectors, positional order is obvious. Once you add a third or fourth input, naming each one eliminates ordering bugs and makes the intent explicit for anyone reading the code later. This same naming discipline carries over naturally to the MoreArgs parameter, which handles the case where some arguments should not vary at all across iterations.
Fixed additional arguments with moreArgs
Named arguments vary per iteration, but sometimes you need a parameter that stays fixed across every call. Rather than wrapping your function in another anonymous function that hardcodes the constant, you can use MoreArgs to inject the same value into every invocation without cluttering the iteration logic. This keeps the parallel argument vectors clean and focused on what actually varies.
Think of MoreArgs as a way to curry a function inside mapply without writing an explicit wrapper. If your function has five parameters and only two of them need to change per iteration, MoreArgs lets you pin the other three to constants, keeping the call site tidy and readable. This is especially helpful when the fixed arguments are configuration values like file paths, model hyperparameters, or threshold cutoffs that you do not want buried inside an anonymous function body.
multiply <- function(x, y, factor) x * y * factor
mapply(multiply, c(2, 3, 4), c(10, 20, 30), MoreArgs = list(factor = 0.5))
# [1] 10 30 60
Every call to multiply() receives factor = 0.5 in addition to the values from the vectors. The MoreArgs pattern scales naturally when you move from named functions to anonymous ones. Once the logic you need does not already exist as a named function, writing an inline anonymous function inside the mapply call keeps everything self contained without polluting the global namespace with single use helper functions.
Anonymous functions for custom operations
For operations that don’t have a ready-made function, pass an anonymous function:
mapply(function(x, y) seq_len(x) + y, c(a = 1, b = 2, c = 3), c(A = 10, B = 0, C = -10))
# $a
# [1] 11
#
# $b
# [1] 2 3
#
# $c
# [1] -9 -8 -7 -6 -5 -4
The names from the first vector (a, b, c) become the names of the result list elements. The anonymous function receives x from the first vector and y from the second, running parallel iteration across both. When the return values from each invocation all have the same length and type, mapply() will try to collapse them into a matrix or vector by default. You can suppress this simplification to guarantee a list output no matter what shape each individual result takes.
Avoiding simplification
When SIMPLIFY = FALSE, the result is always a list regardless of what FUN returns:
result <- mapply(function(x, y) x^y, c(2, 3), c(4, 5), SIMPLIFY = FALSE)
print(result)
# [[1]]
# [1] 16
#
# [[2]]
# [1] 243
This is useful when your function might return vectors of varying lengths, where automatic simplification would fail or produce unexpected array shapes. A practical application of keeping results as a list is building a collection of data frames from parallel input vectors, where each call to FUN constructs one row or one group level summary. Keeping SIMPLIFY set to FALSE ensures you get back the list of individual frames without R trying to smash them into a ragged array. Once you have the list of data frames, you can bind them together with do.call and rbind to get a single tidy data frame.
The pattern of constructing data frames element by element with mapply is surprisingly common in data cleaning workflows. When source data arrives as separate vectors for each column, mapply gives you a functional alternative to building the data frame with repeated subset assignment inside a for loop. The result is a list of one row data frames that you can combine with a single bind call after the iteration completes.
Creating a data frame from parallel vectors
You can use mapply() to construct a data frame row-by-row:
names <- c("Alice", "Bob", "Carol")
ages <- c(32, 45, 28)
scores <- c(85.5, 92.0, 78.3)
mapply(function(n, a, s) data.frame(name = n, age = a, score = s),
names, ages, scores, SIMPLIFY = FALSE)
# [[1]]
# name age score
# 1 Alice 32 85.5
# ...
Building data frames with mapply is a natural fit when your input data arrives as parallel vectors from different sources. The same parallel iteration pattern also replaces many common for loops where you would otherwise write sequential assignment into a preallocated result vector. Recognizing when a loop can become a single mapply call is one of the more productive refactoring habits in base R programming.
Common patterns
Using mapply() instead of a for loop: When you find yourself writing a for loop to combine multiple vectors element-by-element, mapply() often provides a cleaner alternative.
# Instead of this:
x <- c(1, 2, 3)
y <- c(10, 20, 30)
result <- numeric(length(x))
for (i in seq_along(x)) {
result[i] <- x[i] + y[i]
}
# Do this:
result <- mapply(function(x, y) x + y, x, y)
Controlling output with SIMPLIFY: The SIMPLIFY argument mirrors the behavior of sapply(). Use SIMPLIFY = FALSE when you need predictable list output and SIMPLIFY = TRUE (the default) when you know your function always returns scalars of the same type.
Gotchas
Recycling with mismatched lengths: If argument lengths aren’t multiples of each other, R recycles the shorter one and emits a warning:
mapply(paste, c("a", "b", "c"), c(1, 2), USE.NAMES = FALSE)
# Warning message:
# longer object length is not a multiple of shorter object length
Empty arguments: If any ... argument has length zero, mapply() returns a list of length zero. While the recycling warning is a soft signal that your input vectors don’t align cleanly, passing an empty vector is a hard stop — the function performs zero iterations and returns immediately. This edge case trips up scripts that dynamically filter data before passing it to mapply(); always guard against empty inputs when the vector lengths depend on runtime conditions.
mapply(rep, 1:3, integer())
# list()
Confusion with Hmisc::mApply: The Hmisc package exports a function also called mApply() that operates on matrix margins. If you load both packages, the base version takes precedence, but this can cause confusion in code that expects the Hmisc behavior.
See also
- sapply(), univariate version with optional simplification
- lapply(), iterate over a single list or vector
- apply(), apply a function over margins of an array or matrix
- Map, identical to
mapply()withSIMPLIFY = FALSE - purrr::pmap — tidyverse equivalent for any number of arguments