flowchart LR
L["A List"] --> AP["Append <br> c(), append()"]
L --> NE["Nest & Unnest <br> nested lists, rapply()"]
L --> IT["Iterate <br> lapply / sapply / vapply"]
L --> FN["Functional <br> Map / Reduce / Filter"]
style L fill:#e3f2fd,stroke:#1976D2
style AP fill:#fff3e0,stroke:#F57C00
style NE fill:#fff3e0,stroke:#F57C00
style IT fill:#f3e5f5,stroke:#8E24AA
style FN fill:#f3e5f5,stroke:#8E24AA
13 Operations on Lists: Appending, Nesting, and Manipulation
This chapter goes beyond building lists and covers the operations that make them genuinely useful. You will learn how to append elements onto a list, how to combine two lists, how to work with nested lists where elements are themselves lists, and how to search, filter, and flatten them. You will meet the core iteration family for lists, lapply(), sapply(), and vapply(), which applies a function to every element and gives you back a list or a vector. You will also see Map(), Reduce(), and Filter(), which are R’s functional-programming cousins, and the modern alternatives in the purrr package. By the end of this chapter you will be able to treat a list as a small database and extract information from it without writing explicit loops.
13.1 Appending to a List
c() on Two Lists: Flatten at the Top Level
c() does not just combine vectors; it also combines lists. When you pass two lists to it, the top-level elements of both are concatenated into a new list.
append() for Inserting at a Given Position
append(x, values, after) inserts new elements after a chosen position. It is handy when the order of elements matters.
c(a, new_val) When new_val Is Not a List
If you want to append a vector as a single list element, wrap it in list(...). Otherwise c() flattens the vector into individual list elements.
13.2 Nested Lists
List elements can themselves be lists. That lets you model arbitrary tree-shaped data: a report with sections and subsections, a configuration with categories, a JSON-like object returned from an API.
$ or [[
To access nested elements, chain the access operators.
Chains like config$authors$primary$name work, but they break the moment any intermediate element is missing or renamed. In production code, use helpers like purrr::pluck(config, "authors", "primary", "name", .default = NA) that fail safely, or flatten the list to a data frame so each value has one canonical path.
13.3 Iterating over a List: lapply(), sapply(), vapply()
R’s apply family lets you do to a list what vectorised arithmetic does to a vector: apply an operation to every element without writing a for loop.
| Function | Returns |
|---|---|
lapply(x, FUN) |
A list of the same length as x, one result per element. |
sapply(x, FUN) |
A simplified vector or matrix when possible; otherwise falls back to a list. |
vapply(x, FUN, FUN.VALUE) |
Same as sapply() but you declare the expected return type up front. |
vapply() for Production Code
sapply() is convenient because you rarely specify the output type; but that convenience is also a risk. If the data changes and one element returns something unexpected, sapply() silently gives you a different kind of object. vapply() rejects the mismatch, which is the kind of loud failure you want in a pipeline.
13.4 Anonymous Functions Inside lapply()
You often do not need to name the function you are applying. R has always accepted function(x) ... in place, and from R 4.1 onwards the shorthand \(x) ... is available.
13.5 Map(), Reduce(), Filter()
| Function | Purpose |
|---|---|
Map(f, a, b, ...) |
Apply f element-wise to several lists or vectors in parallel. |
Reduce(f, x) |
Combine the elements of x pairwise, left to right. |
Filter(p, x) |
Keep the elements of x for which the predicate p returns TRUE. |
Map() Output Into a Vector
Map() always returns a list. Wrap the call in unlist() (or vapply()) if you want a vector back.
13.6 Searching and Filtering Lists
When you find yourself holding a list of like-shaped lists (a list of student records, a list of products), ask whether a data frame would serve you better. Data frames give you column-wise access, dplyr verbs, and automatic printing. Lists of records are essential for truly heterogeneous items, but tabular data wants a table.
13.7 Flattening Nested Lists
unlist() Flattens All the Way
unlist() walks into every nested list and produces a single atomic vector, subject to the usual coercion rules. Pass recursive = FALSE if you want only the top-level elements flattened.
rapply(): Apply a Function Recursively
rapply() walks a nested list and applies a function to every atomic leaf. It is the apply-family member for nested structures.
13.8 A Worked Example: A Mini Gradebook
Imagine a small gradebook where each student is a nested list.
Every technique from this chapter appears: nesting, iteration with sapply(), filtering with Filter(), computing subject means with nested sapply() calls, and appending a record with c().
13.9 Summary
| Concept | Key Takeaway |
|---|---|
| Combining lists | c() concatenates top-level elements; append() inserts at a position. |
| Nesting | Lists can contain lists; access with chained $ or [[. |
lapply() |
Apply a function to each element; always returns a list. |
sapply() |
Like lapply() but simplifies when it can; watch for type surprises. |
vapply() |
sapply() with an asserted return type, safer for production code. |
| Anonymous functions | function(x) ... or \(x) ... inside apply calls. |
Map(), Reduce(), Filter() |
Functional helpers for parallel apply, folding, and filtering. |
| Flattening | unlist() for atomic leaves; rapply() for deep nested work. |
| Lists of records | When every element has the same shape, a data frame is usually better. |
Mastering lapply() / sapply() is the single biggest step you can take toward idiomatic R. Any time you are tempted to write a for loop that fills a vector, ask whether one of the apply-family functions fits. In the next chapter you will meet the matrix, the other 2-D structure in R, and the one you reach for whenever every cell in your table shares a type.