Functional Programming with purrr
purrr is the tidyverse's functional-programming toolkit: it lets you apply a function to every element of a list or vector with map() , replacing hand-written loops with short, type-safe, composable pipelines.
Learn Functional Programming with purrr in our free R course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…
Part of the free R course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
In this lesson you'll learn map() and its typed variants, the ~ .x formula shorthand, parallel iteration with map2() / pmap() , folding with reduce() , and filtering with keep() / discard() — and how they compare to the base apply family.
What You'll Learn in This Lesson
1️⃣ map() and the Typed Variants
The core verb is map() : give it a list or vector and a function, and it runs the function on each element, returning a list of the same length. You can pass a function by name, write one inline, or use the formula shorthand.
A plain map() always returns a list, which is awkward if you wanted a numeric vector. The typed variants return an atomic vector of exactly that type — and error loudly if a result doesn't fit, catching bugs early.
2️⃣ The ~ .x Shorthand & Parallel Maps
For short transformations, the ~ formula is the idiomatic purrr style: .x stands for the current element. It's just a compact way to write an anonymous function.
When you need to walk two or more vectors in lockstep, reach for map2() (exactly two, with .x and .y ) or pmap() (any number, passed as a list).
3️⃣ reduce(), keep() & discard()
Not every job is one-to-one. reduce() folds a whole list into a single value, and keep() / discard() filter a list by a predicate. Together with map() , they let you express most iteration as one readable pipe.
Notice the final pipe: keep then map_dbl then reduce . Each verb does one thing, and the data flows left to right — far easier to read and modify than a nested for loop.
Your turn. Fill in the # TODO blank and run it.
Mini-Challenge: Per-Group Means
This is the everyday purrr task: summarising a list of vectors without a single loop. Combine map_dbl() , map_int() , keep() , and reduce() .
Common Errors (and the fix)
- ❌ map_dbl() errors with "must be a single double" — ✅ your function returned the wrong type or length; use plain map() , or fix the function to return one number.
- ❌ Using .x outside a ~ formula — ✅ .x only exists inside purrr's formula shorthand; in a normal function(x) use x .
- ❌ map2() length mismatch — ✅ the two inputs must be the same length (or one of length 1); check with length() .
- ❌ Forgetting to library(purrr) — ✅ load the package (or library(tidyverse) ) before using map() .
Pro Tips
- 💡 Extract by name or position: map_chr(records, "name") pulls the name element from each list — a shortcut for ~ .x$name .
- 💡 Build data frames: map_dfr() row-binds each result into one tibble — ideal for reading many files.
- 💡 Side effects only: use walk() instead of map() when you just want to print or save and don't need the return value.
📋 Quick Reference — purrr
Task
R Syntax
Map → list
map(x, f)
Map → numeric
map_dbl(x, ~ .x * 2)
Two inputs
map2_dbl(x, y, ~ .x + .y)
Many inputs
pmap_dbl(list(a, b), f)
Fold to one value
reduce(x, )
Filter a list
keep(x, ~ .x > 0)
Frequently Asked Questions
🎉 Lesson Complete!
- ✅ map() applies a function to each element and returns a list
- ✅ Typed variants ( map_dbl , map_chr , …) return a safe atomic vector
- ✅ ~ .x is shorthand for an anonymous function
- ✅ map2() / pmap() iterate over several inputs at once
- ✅ reduce() folds to one value; keep() / discard() filter
- ✅ Next lesson: Dates with lubridate
Practice quiz
What does a bare map() always return?
- A numeric vector
- A list, one element per input
- A data frame
- A single value
Answer: A list, one element per input. map() always returns a list of the same length as its input.
Which variant guarantees a double (numeric) vector or errors?
- map_dbl()
- map()
- map_chr()
- map_lgl()
Answer: map_dbl(). map_dbl() returns a double vector and errors if a result is not a single double.
In a ~ formula, what does .x refer to?
- The output list
- The function name
- The current element being processed
- The index position
Answer: The current element being processed. .x is the current element inside purrr's formula shorthand.
Which function walks TWO vectors in parallel?
- pmap()
- map()
- reduce()
- map2()
Answer: map2(). map2() iterates over exactly two vectors, exposing .x and .y.
What does reduce(1:5, ) return?
- 15
- A list of 5 sums
- 120
- 5
Answer: 15. reduce() folds with + : 1+2+3+4+5 = 15.
Which helper keeps only elements matching a predicate?
- discard()
- keep()
- map_lgl()
- filter()
Answer: keep(). keep() retains elements for which the predicate is TRUE; discard() removes them.
Which typed variant returns a character vector?
- map_int()
- map_dbl()
- map_chr()
- map_lgl()
Answer: map_chr(). map_chr() returns a character vector, erroring on a type mismatch.
How do map() and reduce() differ?
- map() folds to one value; reduce() is one-to-one
- map() is one-to-one; reduce() folds many into one value
- They are identical
- reduce() only works on numbers
Answer: map() is one-to-one; reduce() folds many into one value. map() transforms each element independently; reduce() collapses the list to a single value.
Why are typed map_*() variants safer than base sapply()?
- They run faster
- They print nicer output
- They promise an exact type or error, never silently guess
- They avoid loading packages
Answer: They promise an exact type or error, never silently guess. sapply() guesses its return type; the typed map variants guarantee one or fail loudly.
How do you access the value of a reactive-style purrr formula's second input in map2()?
- .z
- .x only
- .y
- ..2 only
Answer: .y. In map2(), .x is the first vector's element and .y the second's.
Continue this course
- Previous: Reshaping: pivot_longer & pivot_wider
- Next: Dates with lubridate