rguides

purrr::map2

Overview

map2() applies a function to pairs of elements from two vectors or lists, running the function once per position. It’s the two-input version of map(). If you need to iterate over more than two inputs simultaneously, see pmap() instead.

Like map(), map2() returns a list by default, with type-specific variants (map2_dbl(), map2_chr(), etc.) that return atomic vectors directly.

Installation

library(purrr)

Loading the purrr package makes all the mapping functions available. The core function signature reveals the key parameters: .x and .y for the two parallel inputs, .f for the function to apply, and ... for extra arguments forwarded to each call. The type-specific variants like map2_chr() and map2_dbl() follow the same interface but enforce a particular return type, which catches mismatches early and makes your code self-documenting.

Signature

map2(.x, .y, .f, ...)
map2_lgl(.x, .y, .f, ...)
map2_int(.x, .y, .f, ...)
map2_dbl(.x, .y, .f, ...)
map2_chr(.x, .y, .f, ...)
map2_df(.x, .y, .f, ...)

Parameters

ParameterDescription
.xFirst vector or list to iterate over.
.ySecond vector or list to iterate over. Must be the same length as .x.
.fA function, formula, or atom.
...Additional arguments passed to .f for each call.

Return value

map2() returns a list the same length as .x and .y. Type-specific variants return atomic vectors. Both inputs must have the same length, if they differ, map2() raises an error.

Basic usage

Pairwise combination

first_names <- c("Harry", "Ron", "Hermione")
last_names  <- c("Potter", "Weasley", "Granger")

map2_chr(first_names, last_names, ~ paste(.x, .y))
# [1] "Harry Potter" "Ron Weasley" "Hermione Granger"

The formula interface ~ paste(.x, .y) is a compact way to write an anonymous function where .x refers to the first input and .y to the second. This pattern works well for string operations, but map2() becomes especially useful when you need to perform numeric calculations on paired vectors, such as multiplying prices by their corresponding tax rates element by element.

Element-wise arithmetic

prices  <- c(10.99, 5.49, 8.99)
tax_rate <- c(0.08, 0.10, 0.08)

map2_dbl(prices, tax_rate, ~ .x * .y)
# [1] 0.8792 0.5490 0.7192

Numeric transformations like the tax calculation above are a natural fit for map2_dbl(), which returns a double-precision vector directly without wrapping the result in a list. When the function you are applying needs extra parameters beyond the two iterated inputs — such as an API key or a base URL prefix — you can pass those as additional arguments through the ... dots, which forward them to .f on every call without cluttering the formula.

Passing additional arguments

base_url <- c("https://api.example.com", "https://api.test.com")
endpoints <- c("/users", "/products")

map2_chr(base_url, endpoints, ~ paste0(.x, .y, "?key=abc"))
# [1] "https://api.example.com/users?key=abc"
# [2] "https://api.test.com/products?key=abc"

The dots argument makes map2() convenient for passing configuration parameters that stay constant across all iterations. Beyond simple string concatenation, you can operate on entire list elements — multiplying each numeric vector by a scalar, for instance — and the formula shorthand ~ .x * .y makes the intent obvious without the boilerplate of a full anonymous function.

Formula and anonymous function shorthand

numbers <- list(c(1, 2, 3), c(4, 5, 6))
multipliers <- list(10, 100)

map2(numbers, multipliers, ~ .x * .y)
# [[1]]
# [1] 10 20 30
# [[2]]
# [1] 400 500 600

When the two inputs are lists of vectors rather than atomic vectors, the element-wise operations still work the same way, making map2() a natural choice for batch-processing structured data. You can also use map2() to drill into nested list structures, pulling out named fields from two parallel lists and combining them into a single atomic vector with a type-safe variant like map2_dbl().

Extracting paired elements

x <- list(list(a = 1, b = 2), list(a = 3, b = 4))
y <- list(list(a = 5, b = 6), list(a = 7, b = 8))

map2_dbl(x, y, ~ .x$a + .y$b)
# [1]  6 10

Extracting fields from nested structures is a common pattern when you receive JSON responses or work with deeply nested list columns. A different practical scenario is combining corresponding columns from two separate data frames — for instance, building a human-readable label by pasting together a name from one table and a location from another, which is a lightweight alternative to a full join for simple labelling tasks.

Common use cases

Combining columns from two data frames

library(dplyr)

df1 <- tibble(name = c("Alice", "Bob"), age = c(25, 30))
df2 <- tibble(city = c("London", "Berlin"), country = c("UK", "Germany"))

# Combine into a single tibble row-wise
pmap_dfr(list(name = df1$name, city = df2$city, country = df2$country), tibble)
# # A tibble: 2 x 3
#   name  city    country
# 1 Alice London   UK
# 2 Bob   Berlin  Germany

# Or simpler - just use cbind for this case, but map2 helps when you need transformation:
pmap_chr(list(df1$name, df2$city), ~ paste0(.x, " (", .y, ")"))
# [1] "Alice (London)" "Bob (Berlin)"

When you need to bring together columns from separate data frames without a formal join, pmap_chr() with a named list offers a concise alternative. A similar pattern arises when formatting geographic coordinates — passing paired latitude and longitude vectors through map2_chr() with sprintf() produces neatly formatted location strings suitable for map labels or tooltip text.

Parallel element-wise operations

lat <- c(51.5074, 40.7128, 48.8566)
lon <- c(-0.1278, -74.0060, 2.3522)

map2_chr(lat, lon, ~ sprintf("(%.4f, %.4f)", .x, .y))
# [1] "(51.5074, -0.1278)" "(40.7128, -74.0060)" "(48.8566, 2.3522)"

The sprintf() approach gives you precise control over the output format, which is essential when the result must conform to a specific string pattern. Another everyday use of map2() is generating compound labels from two parallel vectors — for instance, combining experimental condition names with their intensity levels to create unique identifiers for each treatment group in a factorial design.

Generating paired labels

x_labels <- c("alpha", "beta", "gamma")
y_labels <- c("low", "medium", "high")

map2_chr(x_labels, y_labels, ~ paste0(.x, "_", .y))
# [1] "alpha_low" "beta_medium" "gamma_high"

Generating labels from paired vectors is a common task in report automation and dashboard pipelines. If you are coming from base R, you may be accustomed to using mapply() for the same purpose. The purrr approach differs in that the return type is declared explicitly by the variant you choose, whereas mapply() attempts to simplify the result automatically, which can lead to unexpected types when the inputs vary in length or structure.

map2 vs mapply

map2() is the tidyverse equivalent of base R’s mapply(). The key difference is syntax:

# Base R mapply
mapply(paste, c("a", "b"), c("1", "2"), MoreArgs = list(sep = "-"))
# [1] "a-1" "b-2"

# purrr map2
map2_chr(c("a", "b"), c("1", "2"), ~ paste(.x, .y, sep = "-"))
# [1] "a-1" "b-2"

mapply() has a SIMPLIFY argument that tries to collapse the result to a matrix or vector. map2() variants make the expected output type explicit.

Gotchas

Length mismatch throws an error. Unlike mapply() which recycles inputs of different lengths (with a warning), map2() requires equal-length inputs:

map2(c("a", "b", "c"), c("1", "2"), paste)
# Error: `.x` and `.y` must be the same size.
#         `.x` has size 3. `.y` has size 2.

The strict length check is one of map2()’s safety features — it catches alignment bugs that would otherwise produce silently incorrect results with recycled values. Another common point of confusion is that map2() only provides the elements themselves to your function, not their positional index. When you need the index alongside the values, switch to imap() for a single input, or wrap both vectors in a named list and use pmap() to iterate by row with explicit names for clarity.

No index argument in map2. If you need the position as well, use imap() for one input or convert to a data frame first:

x <- c("a", "b", "c")
imap_chr(x, ~ paste0(.y, ": ", .x))
# [1] "1: a" "2: b" "3: c"

For two inputs with index, consider using pmap() with a named list or seq_along() for clarity. A final subtlety to watch for is that map2() always returns a list when the function produces outputs of different shapes or lengths per call — it never tries to simplify. If you want a flattened atomic vector, use a typed variant like map2_chr(), but only when you are certain every invocation will return a single character string of consistent length.

map2 always returns list for multiple outputs. If your function returns a different-shaped object per call, map2() preserves the list structure:

x <- list(c(1, 2), c(3, 4, 5))
y <- list(c("a", "b"), c("c", "d", "e"))

map2(x, y, paste)
# [[1]]
# [1] "1 a" "2 b"
# [[2]]
# [1] "3 c" "4 d" "5 e"

map2() iterates over two lists or vectors in lockstep, passing corresponding elements to .f. Both .x and .y must have the same length; unlike base R’s vectorized operations which recycle, map2() raises an error on length mismatch. When you want to pair each element of one vector with every element of another (a cross product), use pmap() with expand.grid() or tidyr::crossing() to generate all combinations first.

Typed variants map2_chr(), map2_dbl(), map2_int(), map2_lgl() enforce the return type and are preferable when the output type is known in advance. map2_dfr() and map2_dfc() row-bind or column-bind data frame results respectively. When the two inputs are columns in a data frame, mutate() with an anonymous function often reads more clearly than map2() called outside a mutate().

See also