purrr map() in R: Apply a Function (Tidyverse Style)
The map() function in purrr applies a function to each element of a list or vector and returns a list. Type-safe variants (map_dbl, map_chr, map_lgl, map_dfr) return specific atomic types or data frames.
map(1:5, ~ .x^2) # list output (lambda) map_dbl(1:5, ~ .x^2) # numeric vector map_chr(1:5, as.character) # character vector map_lgl(1:5, ~ .x > 2) # logical vector map_dfr(files, read.csv) # combine to data frame (rbind) map_dfc(list_of_vec, identity) # combine to data frame (cbind) map2(a, b, ~ .x + .y) # two inputs
Need explanation? Read on for examples and pitfalls.
What map() does in one sentence
map(x, fn) applies fn to each element of x and returns a LIST of results. Type-safe variants like map_dbl() enforce a specific output type and error if the function returns something different.
map() is purrr's answer to base R's lapply(). The main differences: lambda syntax ~ .x + 1, type-safe variants, and tighter integration with the tidyverse.
Syntax
map(.x, .f, ...). .x is the input; .f is the function or lambda; ... is extra args passed to .f.
map_*() variants whenever you know the expected output type. map_dbl() errors if the function returns NA or a non-numeric. This catches bugs early. Reach for plain map() only when output types vary or you genuinely need a list.Five common patterns
1. Plain map (list output)
~ .x * 10 is purrr's lambda syntax. .x is the input element.
2. Type-safe numeric output
map_dbl() returns a numeric vector. Each call must return a single numeric; otherwise it errors.
3. Character output
4. Combine to data frame
map_dfr() calls the function on each input, then row-binds the results into one data frame.
5. Two inputs with map2
map2() (and map2_*() variants) iterates over TWO inputs in lockstep. .x is from the first; .y is from the second.
map_*() type-safe variants are why purrr is preferred over base lapply/sapply. map_dbl(x, fn) is vapply(x, fn, numeric(1))'s tidyverse equivalent: same type-safety, cleaner syntax. For tidyverse users, the map_* family replaces both sapply (auto-simplify) and vapply (type-safe).map() vs lapply() vs sapply() vs vapply()
Four R iteration functions across base and tidyverse, with different output guarantees.
| Function | Package | Output | Type-safe |
|---|---|---|---|
lapply() |
base | List | No |
sapply() |
base | Vector / matrix / list (auto) | No (surprises) |
vapply() |
base | Type-strict vector | Yes |
map() |
purrr | List | No |
map_*() |
purrr | Type-strict atomic | Yes |
map() is essentially lapply() with cleaner lambda syntax. The win is the typed map_*() family: you declare the output type, and a wrong return value errors instead of silently producing a list. This catches bugs early in pipelines.
A practical purrr workflow
Most real purrr usage chains a few common patterns into a single iteration pipeline.
- Read N files:
map_dfr(file_paths, read.csv)returns one combined data frame. - Fit N models:
map(formulas, ~ lm(.x, data = my_data))returns a list of model objects. - Extract per-model summary:
map_dbl(models, ~ summary(.x)$r.squared)pulls a number from each. - Tidy results:
map_dfr(models, broom::tidy, .id = "model")row-binds tidy results.
This pipeline (read, fit, extract, tidy) is the canonical purrr workflow. Each step picks the right map_* variant based on what the function returns.
Common pitfalls
Pitfall 1: forgetting the _dbl / _chr suffix. map(1:5, ~ .x^2) returns a LIST, not a numeric vector. Use map_dbl(1:5, ~ .x^2) for vector output.
Pitfall 2: lambda syntax confusion. ~ .x + 1 is a function definition with .x as the argument. \(x) x + 1 (R 4.1+) is the modern lambda syntax. Both work; pick one and stick with it.
map_dbl and friends ERROR if the function returns NA or wrong type unexpectedly. A function that USUALLY returns a number but occasionally returns NULL crashes map_dbl. Use purrr::possibly() or safely() to wrap the function for graceful error handling.Try it yourself
Try it: Use map_dbl to compute the variance of each numeric column in iris[, 1:4]. Save to ex_vars.
Click to reveal solution
Explanation: map_dbl(iris[, 1:4], var) applies var() to each of the 4 numeric columns. The result is a named numeric vector with one variance per column.
Related purrr functions
After mastering map, look at:
map_dbl(),map_chr(),map_lgl(),map_int(): type-safe variantsmap_dfr(),map_dfc(): combine to data framemap2(),pmap(): multi-input variantswalk(),walk2(): for side effects (no return)safely(),possibly(): error-handling wrappersimap(): map with index access
For pipelines that mostly use base R, lapply and sapply are equivalents.
FAQ
What is the difference between purrr map and base R lapply?
Both apply a function to each element. map() returns a list (like lapply). map_dbl/chr/lgl() are type-safe variants that return specific atomic vectors. purrr also has lambda syntax (~ .x + 1) and tidyverse integration.
How do I write an anonymous function in purrr map?
Use ~ .x + 1 (purrr lambda) or \(x) x + 1 (R 4.1+ native lambda). Both work in purrr. The .x is the input element placeholder.
How do I get a vector instead of list from purrr map?
Use the type-safe variant: map_dbl() for numeric, map_chr() for character, map_lgl() for logical, map_int() for integer. They error if the function returns the wrong type.
How do I apply a function to two inputs at once?
Use map2(): map2(x, y, ~ .x + .y). For more than 2, use pmap() with a list of inputs.
How do I combine map output into a data frame?
map_dfr(x, fn) row-binds the function's outputs (each output should be a data frame). map_dfc(x, fn) column-binds them. Both are common patterns for reading multiple files into one data frame.