Working with lists in purrr

R
intermediate
Published

August 20, 2026

Introduction

purrr is mainly used for functional programme, especially making it easier and more consistent to apply a function over a group of items and collect the output:

more_less <- function(col) {
  if(sum(col) > 200) "Loads"
  else "Not much"
}

library(purrr)

mtcars |>
  map(more_less)
$mpg
[1] "Loads"

$cyl
[1] "Not much"

$disp
[1] "Loads"

$hp
[1] "Loads"

$drat
[1] "Not much"

$wt
[1] "Not much"

$qsec
[1] "Loads"

$vs
[1] "Not much"

$am
[1] "Not much"

$gear
[1] "Not much"

$carb
[1] "Not much"
mtcars |>
  map_vec(more_less)
       mpg        cyl       disp         hp       drat         wt       qsec 
   "Loads" "Not much"    "Loads"    "Loads" "Not much" "Not much"    "Loads" 
        vs         am       gear       carb 
"Not much" "Not much" "Not much" "Not much" 

But purrr also has a set of tools for working with lists. This session is an introduction to those functions. We’ll use the purrr cheatsheet as a starting point for this session.

A reminder about lists

Lists are a data structure in R with two very useful properties. Unlike vectors, they can contain data of different classes. Unlike data frames/tibbles, they can be ragged, containing items of different lengths:

test_list <- list(a = LETTERS, b = NA, c = 1:5, d = NULL, e =  letters)

test_list
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b
[1] NA

$c
[1] 1 2 3 4 5

$d
NULL

$e
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"
  • subset a list by item/index with []:
test_list["c"]
$c
[1] 1 2 3 4 5
test_list[1]
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

** retrieve the contents of a list item with $ or [[]]:

test_list[["c"]]
[1] 1 2 3 4 5
test_list$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

** subset those list contents with chained square brackets:

test_list[["a"]][2]
[1] "B"
test_list$c[3]
[1] 3

modify()

Effectively map for list items. modify() applies a function to each list item and returns a structurally-identical modified list:

test_list |>
  modify(toupper)
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b
[1] NA

$c
[1] "1" "2" "3" "4" "5"

$d
character(0)

$e
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

Select list items to modify with modify_at()/modify_if() / modify_depth():

test_list |>
  modify_at("e", toupper) # select list item by quoted name
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b
[1] NA

$c
[1] 1 2 3 4 5

$d
NULL

$e
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"
test_list |>
  modify_if(is.numeric, \(x) x + 2) # select list item by function
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b
[1] NA

$c
[1] 3 4 5 6 7

$d
NULL

$e
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"
list(list(list(list(deepo = c(3, 4, 5))))) |> # likely to be more trouble than it's worth, especially because many functions will do odd things to list items
  modify_depth(4, \(x) x * 10)
[[1]]
[[1]][[1]]
[[1]][[1]][[1]]
[[1]][[1]][[1]]$deepo
[1] 30 40 50

reduce()

A bit like cumsum()! Apply a function to each element recursively.

1:10 |>
  reduce(sum) # so think 1+2=3, then 3+3=6, then 6+4=10,...,45+10=55. That can be a bit hard to understand, so accumulate helps by showing intermediate results
[1] 55
1:10 |>
  accumulate(sum)
 [1]  1  3  6 10 15 21 28 36 45 55
list(a = c("space", "star", "dragon"), "fruit") |>
  reduce(paste0) # odd results over vectors, and beware of vector recycling
[1] "spacefruit"  "starfruit"   "dragonfruit"

Vectors

Some purrr functions are designed for vectors:

c(NULL, "hello", NULL, "world") |>
  compact()
[1] "hello" "world"
c("NULL", "hello", "NULL", "world") |>
  keep_at(c(2,4))
[1] "hello" "world"
c("NULL", "hello", "NULL", "world") |>
  discard_at(c(1,3))
[1] "hello" "world"
c("NULL", "hello", "NULL", "world") |>
  set_names(c("one", "two", "three", "four"))
    one     two   three    four 
 "NULL" "hello"  "NULL" "world" 

Predicate functions

test_list |>
  keep(is.numeric)
$c
[1] 1 2 3 4 5
test_list |>
  discard(is.character)
$b
[1] NA

$c
[1] 1 2 3 4 5

$d
NULL
test_list |>
  head_while(is.character)
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"
test_list |>
  tail_while(is.character)
$e
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"
test_list |>
  detect(is.numeric)
[1] 1 2 3 4 5
test_list |>
  detect_index(is.numeric)
[1] 3
test_list |>
  every(is.character)
[1] FALSE
test_list |>
  some(is.character)
[1] TRUE
test_list |>
  none(is.complex)
[1] TRUE
test_list |>
  has_element(1:5)
[1] TRUE

Pluck

test_list |>
  pluck("e")
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"
test_list |>
  pluck("f")
NULL
try({
  test_list |>
    chuck("f") # more surly than pluck
  })
Error in chuck(test_list, "f") : Can't find name `f` in vector.
test_list |>
  pluck_depth()
[1] 2
test_list |>
  assign_in("d", "boo!")
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b
[1] NA

$c
[1] 1 2 3 4 5

$d
[1] "boo!"

$e
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"
test_list |>
  modify_in("c", sqrt)
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b
[1] NA

$c
[1] 1.000000 1.414214 1.732051 2.000000 2.236068

$d
NULL

$e
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"

Reshape and concatenate

test_list |>
  list_flatten() # doesn't do anything: designed to flatten a list back to depth = 1
$a
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b
[1] NA

$c
[1] 1 2 3 4 5

$d
NULL

$e
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"
list("hello", "world") |>
  list_c() # strict about types, wouldn't try and combine our test_list
[1] "hello" "world"
list(list(a = letters, b = LETTERS), list(c = letters, d = LETTERS)) |>
  transpose()
$a
$a[[1]]
 [1] "a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s"
[20] "t" "u" "v" "w" "x" "y" "z"

$a[[2]]
NULL


$b
$b[[1]]
 [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q" "R" "S"
[20] "T" "U" "V" "W" "X" "Y" "Z"

$b[[2]]
NULL
list(1, list(2), list(list(3))) |>
  list_flatten() |> # removes one level each use
  list_flatten()
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3