The purrr map Family
In Lesson 2 you used sapply to summarise Rosewood Bakery's week: its daily units sold per item, like the croissant's c(40, 45, 50, 50, 55, 60). It was the convenient one. The catch was that one line could hand you a vector, a matrix, or a list, depending on the data it happened to see that day. Fine at the console; a quiet bug waiting to happen inside a script.
The purrr package fixes exactly that. It gives you one consistent family of map functions: same "run a function on each piece" idea as the apply family, but where you decide the shape of the answer up front, and R holds it to that promise.
By the end of this lesson you will be able to:
- Run your own function over each element with
map, and know it always returns a list - Pick a typed variant (
map_dbl,map_chr,map_lgl,map_int) to lock down the output shape - Walk two inputs in step with
map2, and several at once withpmap - Reach for
walkwhen you want a function's side effect, not its return value
Prerequisites: Lesson 2 (the apply family, and why sapply's shape can shift), and you can write a function and build a list.
map(): the modern lapply
Rosewood Bakery is back. As in Lesson 2, it tracks daily units sold for each item: one run of numbers per product, bundled in a list. Build this week's list once now (run it):
That is lapply from Lesson 2 with a tidier name: hand map() a collection and a function, and it runs the function on each element and returns a list of the same length, names carried over. Here is the idea precisely. If your list is \(x = (x_1, x_2, \dots, x_n)\) with \(n\) elements (here \(n = 3\)) and \(f\) is the function you supply (here mean), then map computes
\[ \text{map}(x, f) = \big(f(x_1),\, f(x_2),\, \dots,\, f(x_n)\big) \]
and hands the \(n\) answers back in a list. Programmers call this mapping a function over a collection.
When the function you want is not a ready-made name like mean, write it inline. purrr accepts a base R "lambda", \(x) ..., where x is the current item:
.x for the current item: map(sales, ~ max(.x) - min(.x)) does the same thing. Both styles are fine; the \(x) lambda is the newer, more readable one.The typed variants
A list is safe but clunky when every answer is just one number. The typed cousins of map run the same job, then return a plain vector of the type named in the suffix, and they error loudly if any result does not fit that promise. That single guarantee is the cure for sapply's shifting shape.
Each suffix is a contract about one result per item:
| Variant | Each result must be | You get back |
|---|---|---|
map |
anything | a list |
map_dbl |
one number (double) | a numeric vector |
map_int |
one whole number | an integer vector |
map_chr |
one string | a character vector |
map_lgl |
one TRUE or FALSE | a logical vector |
Break the contract and the typed map stops you at once, naming the item that failed, instead of silently bending the shape:
# range() returns TWO numbers, but map_dbl promised one:
map_dbl(sales, range)
#> Error in `map_dbl()`:
#> i In index: 1.
#> i With name: croissant.
#> Caused by error:
#> ! Result must be length 1, not 2.
sapply tries to simplify and quietly returns whatever fits; map_dbl and friends demand the shape you named and fail on the very line that broke it. Predictable output is worth one extra keystroke.Which one gives a vector you can do math on?
You want each item's average daily sales as a plain named numeric vector, so the next line can compute revenue <- averages * price. Which call gives you that, guaranteed?
map_dbl promises one double per item and hands back a named numeric vector, ready for math, and it would error loudly if any item returned something else.sapply's return type depends on the data: it can give a vector, a matrix, or a list. map_dbl guarantees the numeric vector you asked for.map2(): walk two lists in step
So far each call needed one input. But Rosewood wants revenue, and that needs two things lined up: the mean units sold and the price, item by item. map2 walks two inputs in lockstep, handing your function the i-th element of each, together.
The function now takes two arguments because there are two inputs; map2_dbl promises one number back per pair, exactly like map_dbl did for one input. The two inputs must be the same length, and the result is paired by position.
pmap(): walk a whole row of inputs
Two inputs has map2; three or more has pmap (parallel map). You hand pmap a single list of inputs (or, most usefully, a data frame), and it walks them together. With a data frame it takes one row at a time, and each column fills the function argument of the same name.
Matching by column name is what makes pmap the natural tool for "do this for every row of a table". Add a column and your function just gains an argument of that name; nothing else changes.
Best-day revenue, per item
Rosewood wants each item's best single day's revenue: its highest daily units times its price, returned as a named numeric vector. You have sales (the list of daily units) and price (one price per item) from earlier. Two inputs, one number out per item, so reach for map2_dbl, and fill in the function body. (max(x) is the largest value in a vector.)
Show answer
best_day_revenue <- map2_dbl(sales, price, \(x, p) max(x) * p)
best_day_revenue
#> croissant muffin bagel
#> 210.0 80.0 67.5walk(): for side effects
Sometimes you do not want a value back at all. You want a function run for its effect: print a line, save a file, send a message. Using map for that works, but it builds a list of return values you never use (often a list of NULLs). walk is the honest tool: it runs the function purely for the side effect and returns its input, invisibly.
Notice no list printed afterwards: walk returned its input silently. That is by design, so you can drop it into a pipeline and carry on:
map (or a typed variant). If you only want the doing and the results would be noise, use walk.Which verb for a pure side effect?
You want to save a chart to disk for each of Rosewood's three items. Saving a file is a side effect; you do not need any value back. Which verb fits best?
walk runs the function for its effect (saving, printing, messaging) and returns its input invisibly, so it reads as "do this for each", with nothing to collect.map would run the saves, but it also builds a list of return values you do not need (often a list of NULLs): wasted memory and noise for a pure side effect.The same family, two dialects
Everything here has a base R twin from Lesson 2. purrr's win is not new power; it is one consistent set of names and a return type you can rely on.
| Base R (Lesson 2) | purrr | What you get back |
|---|---|---|
lapply |
map |
a list |
sapply (simplify) |
map_dbl, map_chr, map_lgl, map_int |
a vector of THAT type, or a clear error |
vapply |
map_dbl and friends |
a typed, checked vector |
mapply / Map |
map2, pmap |
several inputs walked in step |
a for loop for side effects |
walk |
the input, returned invisibly |
Two honest limits. First, purrr is an extra package dependency; base R's apply family ships with R and needs nothing installed, so for a one-off script sapply/vapply are perfectly fine. Second, map is not the C-level vectorization from Lesson 1: it still runs an R-level loop over the pieces, so for plain arithmetic on a whole vector, prices * 1.08 beats any map. Reach for the map family when each piece needs your own function, and you want a predictable shape back.
References
A few authoritative, free places to take this further:
- purrr: map() reference - the official documentation for
mapand every typed variant, with the exact argument rules. - purrr: map2() and pmap() reference - how
map2,pmap,walkand their typed suffixes line up several inputs. - R for Data Science (2e): Iteration - Hadley Wickham's chapter on iterating with purrr and dplyr, with worked data examples.
- Advanced R (2e): Functionals - the deeper theory:
mapas a functional, and why this style beats hand-written loops.
Lesson 3 complete
You can now iterate the modern way: map for a guaranteed list, the typed map_dbl, map_int, map_chr and map_lgl when you want a specific shape locked down and checked, map2 and pmap to walk two or many inputs in step, and walk for the side-effect jobs where the return value would just be noise. You also know the honest trade-off: purrr buys consistency at the cost of one dependency, and none of it replaces plain vectorization for simple arithmetic.
Next, Lesson 4: Resilient and Nested Iteration. You will keep a map going even when one element throws an error, using safely and possibly, and learn to work with list-columns, whole tables tucked inside a single column, with nest and unnest.