Lists
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Everything on this page is base R (base, utils and stats), so no add-on package version applies; the package jsonlite is named in the section on JSON-shaped data but nothing from it is run, and the lintr 3.4.0 default linters were used only to check the scripts. A list is the data structure of R with the fewest restrictions: its elements can have any type and any length, and an element can be another list. That makes it the structure behind records, nested configuration, the result of almost every function that returns more than one thing, S3 objects and, underneath, data frames. This page covers what a list is, the three ways to take things out of it ([, [[ and $) and the errors they signal, how to put things in and take them out, how lists are combined, flattened (unlist()), merged (modifyList()), walked (rapply()), compared and inspected (str()), how to build a data frame from one with list2DF(), and then the three roles that the shelf scenario uses a list for: a record, an S3 object and a table.
Every example below is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines starting with #> are what it printed. The scripts pass the default lintr linters. Each script is shown in several consecutive blocks with prose between them; a block uses the objects created by the blocks above it in the same script. Errors and warnings that a script shows on purpose are caught with tryCatch() and printed as their conditionMessage(), so the text is exactly what R says. The messages are printed in a UTF-8 locale, where R uses curly quotes in a few of them (all.equal() writes the component names between “ and ”); in a locale that is not UTF-8 they are plain quotes. str() writes a tab character after the class name in its first line for a data frame ('data.frame':<tab>16 obs. of 6 variables:), and the blocks below keep it. The examples use the library scenario of the section: books, loans, shelf_today, loan_days and the book class come from the scenario.R file described on the section index, and the scripts that use them start with source("scenario.R").
What a list is
The R Language Definition describes lists in one paragraph: lists, which it also calls generic vectors, are another kind of data storage; they have elements, each of which can contain any type of R object, so the elements do not have to be of the same type; and lists are vectors, with the basic vector types being called atomic vectors where it is necessary to exclude lists (The R Language Definition: Lists). An Introduction to R says the same for beginners: an R list is an ordered collection of objects known as its components, and the components need not be of the same mode or type (An Introduction to R: Lists). Two words in that description decide how lists behave:
-
A list is a vector, so it has a length, it can have names and other attributes, and it is indexed with
[,[[and$. Functions written for vectors, such aslength(),rev(),c(),head()andlapply(), work on lists. -
A list is recursive: its elements can be lists, so a list is a tree. An atomic vector is a flat sequence of values of one type; a list is a flat sequence of references to objects of any type, and that is what lets it hold a tree.
| Question | Answer for a list | Compare with an atomic vector |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
the number of top-level elements |
the number of values |
|
the length of each element, as an integer vector |
|
|
a list of |
|
Source: R documentation: vector
The first script builds a book entry as a list and asks the questions of the table. lengths() is the function to reach for when each element has a length of its own; the help page describes it as getting the length of each element of a list or atomic vector, and it calls length() for each element, so methods on length are considered. It was added in R 3.2.0 (R NEWS: Changes in R 3.2.0).
# A list holds elements of any type, including other lists.
entry <- list(
title = "The Dispossessed",
year = 1974L,
genres = c("Science fiction", "Classic"),
loaned = FALSE
)
typeof(entry)
#> [1] "list"
length(entry)
#> [1] 4
names(entry)
#> [1] "title" "year" "genres" "loaned"
lengths(entry)
#> title year genres loaned
#> 1 1 2 1
vapply(entry, typeof, character(1L))
#> title year genres loaned
#> "character" "integer" "character" "logical"
Source: R documentation: lengths
is.list(), is.vector(), is.recursive() and is.atomic() are four different questions. is.list() asks whether the type is a list (or a pairlist), is.vector() asks whether the object is a plain vector of a given mode with no attributes other than names, is.recursive() asks whether the object can hold other objects, and is.atomic() asks whether it is one of the atomic types. Since R 4.4.0 is.atomic(NULL) is FALSE, so NULL is neither atomic nor a list; the page Missing and Special Values covers NULL.
# Three questions that look alike have different answers for a list.
is.list(entry)
#> [1] TRUE
is.vector(entry)
#> [1] TRUE
is.recursive(entry)
#> [1] TRUE
is.atomic(entry)
#> [1] FALSE
is.vector(entry, mode = "list")
#> [1] TRUE
is.vector(entry, mode = "character")
#> [1] FALSE
Printing a list
The way a list prints is the way to read its structure. An element without a name is labelled with its position, [[1]], an element with a name is labelled $name, and the labels of a nested list are chained: $a$b is the element b of the element a, and [[3]][[1]] is the first element of the third. A NULL element prints as NULL. An empty element name is not a name, so an element that has none among named ones prints with its position. Printing a long, deeply nested list is hard to read, which is why the section on str() below is the habit to build; print() is what the console does automatically, and it calls the print method of each element that has a class, as the shelf records further down show.
# A list can nest, and printing shows the path to each element.
nested <- list(a = list(b = 1:3, c = "x"), 2, list(NULL))
nested
#> $a
#> $a$b
#> [1] 1 2 3
#>
#> $a$c
#> [1] "x"
#>
#>
#> [[2]]
#> [1] 2
#>
#> [[3]]
#> [[3]][[1]]
#> NULL
#>
#>
# A list without names prints [[i]] headers; names print as $name.
list(1, "a", TRUE)
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] "a"
#>
#> [[3]]
#> [1] TRUE
#>
Source: R documentation: print.default
vector("list", n) creates a list of n elements that are all NULL. It is the list counterpart of numeric(n) and character(n) and the way to pre-allocate a list that a loop fills in. list() itself is the constructor for a list of known elements, and it nests: whatever is passed in is stored as one element, so a vector is not spread over several elements. A list can also hold functions, environments, calls and NULL, which is why functions such as lapply() can work on lists of closures, lists of environments and lists of models.
# Pre-allocating with vector("list", n) gives n NULL elements.
slots <- vector("list", 3L)
slots
#> [[1]]
#> NULL
#>
#> [[2]]
#> NULL
#>
#> [[3]]
#> NULL
#>
length(slots)
#> [1] 3
# A list may even hold functions, environments and other lists of lists.
mixed <- list(sum, emptyenv(), quote(x + y), NULL, list())
vapply(mixed, typeof, character(1L))
#> [1] "builtin" "environment" "language" "NULL" "list"
lengths(mixed)
#> [1] 1 0 3 0 0
Source: R documentation: list
Extraction: single bracket, double bracket and dollar
A list has three extraction operators, and the difference between them is the most important fact about lists. The help page for the operators states it in two sentences: indexing by [ selects a list of the specified elements, and both [[ and $ select a single element of the list; the main difference between the last two is that $ does not allow computed indices, whereas [[ does (R documentation: Extract). The mnemonic that is often used is the train and the passenger: [ gives you a smaller train (still a list), [[ gives you what is in the carriage.
| Operator | Selects | Returns | Index |
|---|---|---|---|
|
any number of elements |
a list (a sub-list), with the names of the selected elements |
positive or negative integers, logical, character; may be a vector |
|
exactly one element |
the element itself, whatever its type |
one number or one string, or a vector of them for recursive indexing |
|
exactly one element, by name |
the element itself |
a literal name, never a computed one; partial matching of the name |
Source: R documentation: Extract
The next block takes entry apart in all three ways. entry["year"] is a list of length one that has an element called year; entry[["year"]] is the integer 1974L. The class() of each shows the difference: "list" against "integer". A sub-list keeps the names of the elements that were selected, a negative index drops elements, and a logical index of the right length keeps the elements where it is TRUE; a list is indexed with the same four kinds of index as an atomic vector, see Subsetting and Indexing.
entry <- list(
title = "The Dispossessed",
year = 1974L,
genres = c("Science fiction", "Classic")
)
# [ returns a list (a sub-list); [[ returns the element; $ is [[ with a name.
entry[1L]
#> $title
#> [1] "The Dispossessed"
#>
entry[[1L]]
#> [1] "The Dispossessed"
entry$title
#> [1] "The Dispossessed"
entry["year"]
#> $year
#> [1] 1974
#>
entry[["year"]]
#> [1] 1974
class(entry[2L])
#> [1] "list"
class(entry[[2L]])
#> [1] "integer"
# [ takes vectors of indices, [[ takes exactly one.
entry[c("title", "year")]
#> $title
#> [1] "The Dispossessed"
#>
#> $year
#> [1] 1974
#>
entry[-1L]
#> $year
#> [1] 1974
#>
#> $genres
#> [1] "Science fiction" "Classic"
#>
entry[c(TRUE, FALSE, TRUE)]
#> $title
#> [1] "The Dispossessed"
#>
#> $genres
#> [1] "Science fiction" "Classic"
#>
$ takes a name that is written in the code, not a value; x$field is the element called field, even when a variable called field exists. To use a name that is held in a variable, use [[. This is the most frequent error when a loop over field names uses $: it silently returns NULL. For the same reason $ is convenient at the console and in a function that knows its field names, and [[ is the one to use in generic code.
# [[ and $ work through a character string too.
field <- "genres"
entry[[field]]
#> [1] "Science fiction" "Classic"
entry$field
#> NULL
Source: R documentation: Extract
Nested access and recursive indexing
A list of lists is read by chaining: x$a$b or x[["a"]][["b"]] evaluates the first extraction and applies the next one to the result. The help page for the operators adds that [[ can be applied recursively: if the single index i is a vector of length p, then alist[[i]] is equivalent to alist[[i1]]...[[ip]], provided that all but the final indexing result in a list. The index vector can be numbers or names, and it is also the form of a path that is held in a variable. The final extraction can be of any length-one element, including one that is itself a list.
# Nested access: step by step, or with a vector in [[ (recursive indexing).
catalogue <- list(
fiction = list(
classic = list(title = "Pride and Prejudice", year = 1813L),
modern = list(title = "Loud Roofs", year = 2021L)
),
poetry = list(title = "Paper Moons", year = 2005L)
)
catalogue$fiction$classic$title
#> [1] "Pride and Prejudice"
catalogue[["fiction"]][["modern"]][["year"]]
#> [1] 2021
catalogue[[c("fiction", "classic", "title")]]
#> [1] "Pride and Prejudice"
catalogue[[c(1L, 2L, 1L)]]
#> [1] "Loud Roofs"
catalogue[[c(1L, 2L)]]
#> $title
#> [1] "Loud Roofs"
#>
#> $year
#> [1] 2021
#>
A chain stops at the first NULL. x$a$b is NULL when a is missing, because NULL$b is NULL; this convenient behaviour is also the reason a misspelt field name inside a long chain is not noticed. A recursive index vector is stricter: it signals an error as soon as a step has nothing to descend into.
Missing names, positions and the errors
A name that is not in a list is not an error: the manual states that attempts to extract a non-existent element by name return NULL. A position that is not in the list is an error for [[ (subscript out of bounds), and for [ it gives a list holding a NULL element whose name is NA. The block below also shows the less common errors. Their texts come from the R sources and were captured from R 4.6.1; the first two mention integerOneIndex, which is the name of an internal helper and not something to look for in your own code.
# Missing names and positions.
entry$missing
#> NULL
entry[["missing"]]
#> NULL
entry["missing"]
#> $<NA>
#> NULL
#>
tryCatch(entry[[4L]], error = \(e) conditionMessage(e))
#> [1] "subscript out of bounds"
entry[4L]
#> $<NA>
#> NULL
#>
tryCatch(entry[[c(1L, 2L)]], error = \(e) conditionMessage(e))
#> [1] "subscript out of bounds"
# The empty subscript is an error in R 4.3.0 and later.
tryCatch(entry[[]], error = \(e) conditionMessage(e))
#> [1] "missing subscript"
# Zero, negative and NA subscripts for [[.
tryCatch(entry[[0L]], error = \(e) conditionMessage(e))
#> [1] "attempt to select less than one element in integerOneIndex"
tryCatch(entry[[-1L]], error = \(e) conditionMessage(e))
#> [1] "attempt to select more than one element in integerOneIndex"
tryCatch(entry[[-1]], error = \(e) conditionMessage(e))
#> [1] "invalid negative subscript in get1index <real>"
tryCatch(entry[[integer()]], error = \(e) conditionMessage(e))
#> [1] "attempt to select less than one element in get1index"
entry[[NA]]
#> NULL
entry[NA_integer_]
#> $<NA>
#> NULL
#>
list(1, 2)[[-1L]]
#> [1] 2
# $ does not work on atomic vectors, [[ with a missing name does not either.
v <- c(a = 1, b = 2)
tryCatch(v$a, error = \(e) conditionMessage(e))
#> [1] "$ operator is invalid for atomic vectors"
v[["a"]]
#> [1] 1
tryCatch(v[["z"]], error = \(e) conditionMessage(e))
#> [1] "subscript out of bounds"
v["z"]
#> <NA>
#> NA
NULL$a
#> NULL
tryCatch(sum$a, error = \(e) conditionMessage(e))
#> [1] "object of type 'builtin' is not subsettable"
tryCatch(mean$a, error = \(e) conditionMessage(e))
#> [1] "object of type 'closure' is not subsettable"
Source: R documentation: Extract
| Expression | Result | Reason |
|---|---|---|
|
|
a missing name on a list is |
|
|
|
|
|
|
|
|
|
|
|
a negative index leaves two; for a list of two it selects the other one |
|
|
the same situation reported by the code path for doubles |
|
|
an index of length zero |
|
|
since R 4.3.0 an empty |
|
|
an |
|
|
|
|
|
a function is not a container |
|
|
|
Source: R documentation: Extract
Two of the rows are changes of the last years. The NEWS of R 4.3.0 says that c(a = 1, 2)[[]] no longer matches 2 but rather signals a classed error, and that for consistency NULL[[]] is also erroneous now, as is x[[]] <- v (an error of the same class, "MissingSubscriptError"). The NEWS of R 4.0.0 says that x[[Inf]] returning NULL no longer leads to undefined behaviour, and that x[[-Inf]] and x[[-n]] give more helpful error messages (R NEWS: Changes in R 4.3.0 and 4.0.0). The NA row follows the help page: an NA index extracts an NA from an atomic vector, and NULL from a list.
Partial matching with $
$ is the one extraction operator that matches names partially: x$ti finds title when it is the only name that starts with ti. The help page says that x$name is equivalent to x[["name", exact = FALSE]], that [[ does not partial-match unless exact = FALSE, that exact = NA allows partial matching with a warning, and that the default behaviour is to use partial matching only when extracting from recursive objects (except environments) by $. getElement(x, name) is x[[name, exact = TRUE]] and is the exact accessor that also works on S4 objects. The option warnPartialMatchDollar turns every partial match with $ into a warning; it is worth setting while developing code that handles lists of records, where a partial match is almost always a bug. The reverse is the usual reason to write the full name: a record that later gains a field title_sort makes x$ti ambiguous and returns NULL.
# $ partial-matches a name, [[ does not unless exact = FALSE.
partial <- list(title = "Notes on Gravity", year = 2017L)
partial$ti
#> [1] "Notes on Gravity"
partial[["ti"]]
#> NULL
partial[["ti", exact = FALSE]]
#> [1] "Notes on Gravity"
tryCatch(partial[["ti", exact = NA]], warning = \(w) conditionMessage(w))
#> [1] "partial match of 'ti' to 'title'"
getElement(partial, "ti")
#> NULL
Putting things in and taking them out
The three operators have assignment forms, [<-, [[<- and $<-, and they follow the same division of labour: x[i] <- value replaces a slice and expects a list on the right when the slice should hold list elements, x[[i]] <- value and x$name <- value replace a single element. Assigning to a name that does not exist adds an element at the end; assigning to a position beyond the end extends the list and pads the gap with NULL. A name is an attribute, so an element added by position has an empty name when the other elements have names. The assignments follow copy-on-modify like every other R object (see Assignment, Names and Values): a list that is modified is copied shallowly, which means the new list refers to the same element objects as the old one for every element that was not changed, so modifying a long list does not duplicate its contents. The page Memory and Copy Semantics has the details.
# Adding, replacing and deleting elements by name and by position.
x <- list(a = 1, b = "two")
x$c <- c(3, 3, 3)
x[["d"]] <- list(4)
x[5L] <- list("five")
names(x)
#> [1] "a" "b" "c" "d" ""
x[["a"]] <- 100
x$a
#> [1] 100
str(x)
#> List of 5
#> $ a: num 100
#> $ b: chr "two"
#> $ c: num [1:3] 3 3 3
#> $ d:List of 1
#> ..$ : num 4
#> $ : chr "five"
Deleting with NULL, keeping with list(NULL)
The R FAQ has an entry for the most surprising consequence. FAQ 7.1, "How can I set components of a list to NULL?", says: use x[i] <- list(NULL) to set component i of the list x to NULL, similarly for named components, and do not set x[i] or x[[i]] to NULL, because this will remove the corresponding component from the list (R FAQ 7.1: How can I set components of a list to NULL?). The help page for the operators puts it as a rule for all three replacement forms: a value of NULL deletes the corresponding item of the list, and to set entries to NULL you need x[i] <- list(NULL).
| Assignment | Effect on x |
After |
|---|---|---|
|
deletes the element |
one element fewer; the other positions shift down |
|
deletes every selected element |
as many elements fewer as were selected |
|
keeps the slot and stores a |
the length is unchanged; |
|
stores |
the length is unchanged |
|
stores a list that holds a |
the element is |
# Assigning NULL with [[<- or $<- deletes an element.
x$c <- NULL
x[["d"]] <- NULL
names(x)
#> [1] "a" "b" ""
length(x)
#> [1] 3
# Assigning list(NULL) keeps the slot and stores a NULL in it.
x["b"] <- list(NULL)
names(x)
#> [1] "a" "b" ""
x$b
#> NULL
is.null(x$b)
#> [1] TRUE
length(x)
#> [1] 3
# [<- with NULL deletes several elements at once; list(NULL) keeps them.
y <- list(a = 1, b = 2, c = 3, d = 4)
y[c("a", "b")] <- NULL
names(y)
#> [1] "c" "d"
z <- list(a = 1, b = 2, c = 3)
z[c("a", "b")] <- list(NULL)
z
#> $a
#> NULL
#>
#> $b
#> NULL
#>
#> $c
#> [1] 3
#>
The same rule explains a puzzle: a loop that builds a list of results and assigns NULL for the rows that gave nothing ends up shorter than the number of iterations and with the later results moved to different positions. Assign list(NULL) with [, or collect the results with lapply() or vapply(), which keep a NULL result in its slot. In the opposite direction, to drop the NULL elements of a list, use Filter(Negate(is.null), x) or x[!vapply(x, is.null, logical(1L))]; unlist() also drops them, together with the structure.
Assigning deeper, and into NULL
An assignment through a chain, config$loans$limit <- 3L, is evaluated as a sequence of $ extractions followed by a replacement at each level, which R rewrites into nested calls of $<- (the page Assignment, Names and Values shows the rewrite). When an intermediate element does not exist it is NULL, and assigning with $<- into a NULL first turns it into a list, so the chain creates the missing levels as named lists. That is how a nested configuration is built up without writing the nesting out, and also how a misspelt level creates a new branch instead of failing.
The recursive form of [[<- takes a vector of names or positions, exactly as the extraction form does: x[[c("fiction", "classic", "year")]] <- 1813L replaces a leaf and adds a new one when its name is new. With $ the intermediate levels must be lists or NULL; if one is an atomic vector, x$a$b <- 2 gives the warning Coercing LHS to a list (the text was captured from R 4.6.1, see the section Lists and other types), whereas the recursive form x[[c("a", "b")]] <- 2 adds a named element to the vector.
The behaviour of an assignment into NULL changed in R 4.0.0. The help page for the operators says: when $<- is applied to a NULL x, it first coerces x to list(); this is also what happens with [[<-, where in R versions before 4.0.0 a length one value resulted in a length one (atomic) vector. The NEWS of R 4.0.0 gives the reason: N <- NULL; N[[1]] <- val now turns N into a list also when val has length one, which enables dimnames(r1)[[1]] <- "R1" for a one-row matrix (R NEWS: Changes in R 4.0.0). A tutorial that shows x <- NULL; x[["a"]] <- 1 giving the numeric vector 1 describes R 3.x.
# Assigning beyond the end pads with NULL.
w <- list(1, 2)
w[[5L]] <- "five"
lengths(w)
#> [1] 1 1 0 0 1
# A deeper assignment creates the intermediate lists that are missing.
config <- list()
config$loans$limit <- 3L
config$loans$days <- 21L
config$notify$email <- TRUE
str(config)
#> List of 2
#> $ loans :List of 2
#> ..$ limit: int 3
#> ..$ days : int 21
#> $ notify:List of 1
#> ..$ email: logi TRUE
# Assigning to NULL: since R 4.0.0 a length-one value makes a list.
n1 <- NULL
n1[[1L]] <- "a"
n1
#> [[1]]
#> [1] "a"
#>
n2 <- NULL
n2$title <- "Notes on Gravity"
n2
#> $title
#> [1] "Notes on Gravity"
#>
n3 <- NULL
n3[["x"]] <- 1:3
n3
#> $x
#> [1] 1 2 3
#>
# Recursive assignment with a vector index replaces a nested element.
catalogue <- list(fiction = list(classic = list(title = "Pride and Prejudice",
year = 1812L)))
catalogue[[c("fiction", "classic", "year")]] <- 1813L
catalogue$fiction$classic$year
#> [1] 1813
catalogue[[c("fiction", "classic", "pages")]] <- 432L
str(catalogue)
#> List of 1
#> $ fiction:List of 1
#> ..$ classic:List of 3
#> .. ..$ title: chr "Pride and Prejudice"
#> .. ..$ year : int 1813
#> .. ..$ pages: int 432
Source: R documentation: Extract
Building and combining lists
There are three ways to put lists together and they behave differently, which is a frequent source of nested lists that were meant to be flat:
-
list(a, b)makes a list of two elements, each of which isaorb. Lists are nested. -
c(a, b)joins. When any argument is a list the result is a list, and its elements are the elements of the list arguments (not the lists themselves) and one element per value of the atomic arguments, in the order of the arguments (soc(list(1), 2:3)has three elements). Names are kept. This is the operation described in An Introduction to R under "Concatenating lists" (An Introduction to R: Concatenating lists). -
append(x, values, after)isc()with an insertion point. It works on lists the same way, sovaluesmust itself be a list when it is to be inserted as one element (list("inserted")), otherwise a character vector is spread over several elements.
The help page for c() also has an argument recursive: with recursive = TRUE, the arguments are flattened as unlist() would; the section on unlist() shows the equivalence.
# list() nests its arguments, c() on lists concatenates them.
a <- list(1, 2)
b <- list(3)
length(list(a, b))
#> [1] 2
length(c(a, b))
#> [1] 3
str(list(a, b))
#> List of 2
#> $ :List of 2
#> ..$ : num 1
#> ..$ : num 2
#> $ :List of 1
#> ..$ : num 3
str(c(a, b))
#> List of 3
#> $ : num 1
#> $ : num 2
#> $ : num 3
# c() with a mixture of lists and vectors gives a list.
str(c(list(1), 2:3, "x"))
#> List of 4
#> $ : num 1
#> $ : int 2
#> $ : int 3
#> $ : chr "x"
str(c(list(a = 1), b = list(2), c = 3))
#> List of 3
#> $ a: num 1
#> $ b: num 2
#> $ c: num 3
# c() on a list and a one-element vector adds one element, not a sublist.
c(list(title = "Paper Moons"), year = 2005L)
#> $title
#> [1] "Paper Moons"
#>
#> $year
#> [1] 2005
#>
# append() inserts at a position; rev() reverses the elements.
queue <- list("first", "second", "third")
str(append(queue, list("inserted"), after = 1L))
#> List of 4
#> $ : chr "first"
#> $ : chr "inserted"
#> $ : chr "second"
#> $ : chr "third"
str(rev(queue))
#> List of 3
#> $ : chr "third"
#> $ : chr "second"
#> $ : chr "first"
str(append(queue, "plain"))
#> List of 4
#> $ : chr "first"
#> $ : chr "second"
#> $ : chr "third"
#> $ : chr "plain"
Source: R documentation: c
rev() reverses the elements of a list the same way as of a vector, and c(x, y) or append() is the operation for the other direction (the new element goes at the end or at a position). A list is not a good fit for repeated growth by c() in a loop, because each step copies the list; the first alternative is to create the list at its final size with vector("list", n) and assign each element, and the second is lapply() over the indices, which does the same and returns the list. vapply() is the variant for results that are all of the same type. The two forms are checked for equality in the block below with identical().
# Growing a list in a loop: pre-allocate and fill, rather than c() each time.
n <- 5L
squares <- vector("list", n)
for (i in seq_len(n)) {
squares[[i]] <- i^2
}
unlist(squares)
#> [1] 1 4 9 16 25
squares_apply <- lapply(seq_len(n), \(i) i^2)
identical(squares, squares_apply)
#> [1] TRUE
Source: R documentation: vector
Names are set with names(x) <- value or with setNames(), which is names<- that returns its argument and therefore fits into a pipe or a function call. A list with some names has "" for the others, and a list with no names has NULL names; the page Attributes and Names covers names() in general.
# setNames() and names<- label the elements; unnamed ones get "".
labels <- setNames(list(1, 2, 3), c("one", "two", "three"))
names(labels)
#> [1] "one" "two" "three"
names(list(1, b = 2))
#> [1] "" "b"
unnamed <- list(1, 2)
names(unnamed)
#> NULL
names(unnamed) <- c("x", "y")
unnamed
#> $x
#> [1] 1
#>
#> $y
#> [1] 2
#>
Source: R documentation: setNames
Flattening with unlist()
unlist() goes the other way: it takes a list structure and produces a vector that holds all the atomic components. The help page defines it so: given a list structure x, unlist simplifies it to produce a vector which contains all the atomic components which occur in x. Three arguments control it: recursive (TRUE by default) decides whether list components of x are unlisted too, use.names (TRUE by default) decides whether names are built, and x is typically a list.
The type of the result is decided by coercion. The help page states that where possible the list elements are coerced to a common mode during the unlisting, so the result often ends up as a character vector, and that vectors are coerced to the highest type of the components in the hierarchy NULL < raw < logical < integer < double < complex < character < list < expression. Non-vector elements, such as functions, names, formulas and calls, are not coerced, so a list that contains one stays a list.
| Elements | typeof(unlist(x)) |
Comment |
|---|---|---|
|
|
logical becomes integer |
|
|
integer becomes double |
|
|
the numbers become |
|
|
a function cannot be coerced, so nothing is flattened |
only |
|
the levels are the union of the level sets |
|
|
the class is dropped (see below) |
Source: R documentation: unlist
# unlist() flattens to the simplest common type and builds names.
unlist(list(a = 1, b = 2:3))
#> a b1 b2
#> 1 2 3
unlist(list(a = 1, b = list(c = 2, d = 3:4)))
#> a b.c b.d1 b.d2
#> 1 2 3 4
unlist(list(a = c(x = 1, y = 2), b = c(3, 4)))
#> a.x a.y b1 b2
#> 1 2 3 4
unlist(list(1, 2, 3))
#> [1] 1 2 3
unlist(list(a = 1, b = 2), use.names = FALSE)
#> [1] 1 2
# The result type is the highest type found: logical < integer < double <
# character.
typeof(unlist(list(TRUE, 1L)))
#> [1] "integer"
typeof(unlist(list(TRUE, 1L, 2.5)))
#> [1] "double"
unlist(list(TRUE, 1L, 2.5, "a"))
#> [1] "TRUE" "1" "2.5" "a"
str(unlist(list(1, "a", sum)))
#> List of 3
#> $ : num 1
#> $ : chr "a"
#> $ :function (..., na.rm = FALSE)
Source: R documentation: unlist
Names
With use.names = TRUE the names of the result are built from the names of the list and of its elements. The rule is implemented in compiled code and the help page documents it only by saying that unlist tries to retain the naming information: an element called a that holds one unnamed value gives the name a; one that holds several unnamed values gives a1, a2, and so on; and one that holds named values gives a.x, a.y, joining the names with a dot. An unnamed element inside a named list gets the name of the enclosing element. Two elements with the same name are not made unique. The consequence for a list that is going to be used by name is that unlist() is a convenient way to read a result and a poor way to build a lookup table; if names matter, build them yourself with setNames() or avoid flattening. use.names = FALSE drops all names, and for an atomic vector unlist() does nothing useful, not even drop the names.
The last three blocks of the script show a non-list argument, the empty cases and the equivalent c() form. unlist() on an atomic vector returns it unchanged, NULL elements disappear, and unlist(list()) is NULL. c(..., recursive = TRUE) is unlist() of the arguments put in a list, so the two give the same result, with the difference that c() can take the pieces as separate arguments.
# unlist() on a non-list returns its argument; NULL is dropped.
unlist(1:3)
#> [1] 1 2 3
unlist(list(NULL, 1, NULL))
#> [1] 1
unlist(list())
#> NULL
unlist(list(a = NULL))
#> NULL
# Names and the collisions they can cause.
x <- list(a = 1:2, a = 3)
unlist(x)
#> a1 a2 a
#> 1 2 3
names(unlist(list(1:2, c(p = 3))))
#> [1] "" "" "p"
unlist(list(a = list(1, 2), b = list(x = 3, 4)))
#> a1 a2 b.x b
#> 1 2 3 4
# c(recursive = TRUE) flattens like unlist(), but keeps the arguments apart.
c(list(a = 1, b = list(c = 2, d = 3)), e = 4, recursive = TRUE)
#> a b.c b.d e
#> 1 2 3 4
identical(
c(list(a = 1, b = list(c = 2)), recursive = TRUE),
unlist(list(a = 1, b = list(c = 2)))
)
#> [1] TRUE
Source: R documentation: unlist
recursive = FALSE and the cases that look like bugs
recursive = FALSE flattens one level only; elements that are lists are spliced into the result as list elements, and the names are joined in the same way. This is the idiom for turning a list of lists into one list of the inner elements. Two behaviours of unlist() surprise users of dates and factors. A list of Date objects loses the class, because unlist() does not know about the Date class and returns the underlying number of days; do.call(c, x) keeps the class because c() dispatches to c.Date. A list of factors is the opposite: unlist() is documented to combine factors, and the result is a factor whose levels are the union of the levels. c() combines factors in the same way since R 4.1.0, when its NEWS entry says that using c() to combine a factor with other factors now gives a factor (R NEWS: Changes in R 4.1.0).
# recursive = FALSE removes one level of nesting only.
nested <- list(a = list(b = 1, c = list(d = 2)), e = 3)
str(unlist(nested, recursive = FALSE))
#> List of 3
#> $ a.b: num 1
#> $ a.c:List of 1
#> ..$ d: num 2
#> $ e : num 3
unlist(nested)
#> a.b a.c.d e
#> 1 2 3
# Factors combine their levels; Dates lose their class.
unlist(list(factor("a"), factor(c("b", "a"))))
#> [1] a b a
#> Levels: a b
unlist(list(as.Date("2026-06-30"), as.Date("2026-07-01")))
#> [1] 20634 20635
do.call(c, list(as.Date("2026-06-30"), as.Date("2026-07-01")))
#> [1] "2026-06-30" "2026-07-01"
Source: R documentation: unlist
Merging nested lists with modifyList()
A nested configuration is the typical list that has to be merged: the defaults of the program, a layer from a file and a layer from the command line, where a layer changes some leaves and leaves the others alone. modifyList(x, val, keep.null = FALSE) is the function in utils for it. The help page defines the result element by element: elements in val which are missing from x are added to x; for elements that are common to both but are not both lists themselves, the component in x is replaced by the one in val; and for common elements that are lists in both, x[[name]] is replaced by modifyList(x[[name]], val[[name]]). That makes it a recursive merge in which the right-hand list wins, and the shape of x is preserved except where val says otherwise.
NULL in val deletes the element in x, consistent with the rule of assignment, unless keep.null = TRUE, in which case the NULL is stored. The argument keep.null was added in R 3.0.2; its NEWS entry says it allows NULL components in the replacement to be retained, instead of causing corresponding components to be deleted (R NEWS: Changes in R 3.0.2). Both lists must be lists: x has to be a named list, val is checked with is.list(), and its elements without a name are ignored.
# modifyList() merges two named lists, recursing into sublists.
defaults <- list(
loan_days = 21L,
renewals = list(max = 2L, fee = 0),
notify = TRUE
)
user <- list(renewals = list(max = 1L), notify = FALSE)
str(modifyList(defaults, user))
#> List of 3
#> $ loan_days: int 21
#> $ renewals :List of 2
#> ..$ max: int 1
#> ..$ fee: num 0
#> $ notify : logi FALSE
# NULL in the replacement removes the element...
str(modifyList(defaults, list(notify = NULL)))
#> List of 2
#> $ loan_days: int 21
#> $ renewals :List of 2
#> ..$ max: int 2
#> ..$ fee: num 0
# ... unless keep.null = TRUE (since R 3.0.2) stores it.
str(modifyList(defaults, list(notify = NULL), keep.null = TRUE))
#> List of 3
#> $ loan_days: int 21
#> $ renewals :List of 2
#> ..$ max: int 2
#> ..$ fee: num 0
#> $ notify : NULL
# It adds new names, ignores elements without a name, and needs a list.
str(modifyList(defaults, list(branch = "Main hall")))
#> List of 4
#> $ loan_days: int 21
#> $ renewals :List of 2
#> ..$ max: int 2
#> ..$ fee: num 0
#> $ notify : logi TRUE
#> $ branch : chr "Main hall"
identical(modifyList(defaults, list(1)), defaults)
#> [1] TRUE
tryCatch(
modifyList(defaults, 5),
error = \(e) conditionMessage(e)
)
#> [1] "is.list(val) is not TRUE"
Source: R documentation: modifyList
Two details are worth remembering. A non-list replaces a list and the other way round, so modifyList(defaults, list(renewals = 3L)) turns the renewals sublist into a single integer, and modifyList(list(a = 1), list(a = list(b = 2))) replaces the number by a list. Merging several layers is a fold over the layers, which is what Reduce() is for; the order of the layers is the order of precedence.
# Replacing a sublist by a non-list, and the other way round.
str(modifyList(defaults, list(renewals = 3L)))
#> List of 3
#> $ loan_days: int 21
#> $ renewals : int 3
#> $ notify : logi TRUE
str(modifyList(list(a = 1), list(a = list(b = 2))))
#> List of 1
#> $ a:List of 1
#> ..$ b: num 2
# Merging several layers with Reduce().
layers <- list(
list(loan_days = 21L, notify = TRUE),
list(loan_days = 14L),
list(notify = FALSE)
)
str(Reduce(modifyList, layers))
#> List of 2
#> $ loan_days: int 14
#> $ notify : logi FALSE
Walking a tree with rapply()
lapply() visits the top-level elements of a list and nothing more. For the elements that are themselves lists it passes the whole sublist to the function. rapply(object, f, classes = "ANY", deflt = NULL, how = c("unlist", "replace", "list"), ...) is the recursive version: the help page calls it a recursive version of lapply with flexibility in how the result is structured. It applies f to the leaves of the tree (the elements that are not lists) whose class is in classes, and what happens to the rest and to the result is decided by how.
how |
Elements whose class matches | Elements that do not match |
|---|---|---|
|
replaced by |
replaced by |
|
replaced by |
kept as they are; |
|
replaced by |
replaced by |
Source: R documentation: rapply
# lapply() visits the top-level elements only; rapply() recurses.
tree <- list(a = 1:3, b = list(c = "x", d = list(e = 4.5, f = "y")))
str(lapply(tree, length))
#> List of 2
#> $ a: int 3
#> $ b: int 2
rapply(tree, length)
#> a b.c b.d.e b.d.f
#> 3 1 1 1
# how = "unlist" (the default) simplifies; "replace" keeps the shape.
rapply(tree, toupper, classes = "character", how = "unlist")
#> b.c b.d.f
#> "X" "Y"
str(rapply(tree, toupper, classes = "character", how = "replace"))
#> List of 2
#> $ a: int [1:3] 1 2 3
#> $ b:List of 2
#> ..$ c: chr "X"
#> ..$ d:List of 2
#> .. ..$ e: num 4.5
#> .. ..$ f: chr "Y"
# how = "list" replaces the elements that do not match by deflt.
str(rapply(tree, toupper, classes = "character", how = "list"))
#> List of 2
#> $ a: NULL
#> $ b:List of 2
#> ..$ c: chr "X"
#> ..$ d:List of 2
#> .. ..$ e: NULL
#> .. ..$ f: chr "Y"
str(rapply(tree, toupper, classes = "character", deflt = "-", how = "list"))
#> List of 2
#> $ a: chr "-"
#> $ b:List of 2
#> ..$ c: chr "X"
#> ..$ d:List of 2
#> .. ..$ e: chr "-"
#> .. ..$ f: chr "Y"
Source: R documentation: rapply
classes is matched against the class of each leaf, and an integer vector has the class "integer", not "numeric". In the next block the first call, with classes = "numeric", doubles only the double 4.5, and the integer vector 1:3 is left alone; the second call lists both classes. f receives the leaf as its first argument, and the arguments in ... are passed on to it, which is how trimws(s, which = "left") is used in the last call. Three more facts from the help page: f is a function and not the name of a function (unlike lapply()), on purpose; rapply() accepts an expression as well as a list; and in R 3.5.x and earlier object was required to be a list, which was not the case for its list-like components. The NEWS of R 3.6.0 records that rapply() accepts list-like objects of length 2^31 and more (R NEWS: Changes in R 3.6.0).
# classes matches the class of each leaf, so 1:3 is "integer", not "numeric".
str(rapply(tree, \(v) v * 2, classes = "numeric", how = "replace"))
#> List of 2
#> $ a: int [1:3] 1 2 3
#> $ b:List of 2
#> ..$ c: chr "x"
#> ..$ d:List of 2
#> .. ..$ e: num 9
#> .. ..$ f: chr "y"
str(rapply(tree, \(v) v * 2, classes = c("numeric", "integer"),
how = "replace"))
#> List of 2
#> $ a: num [1:3] 2 4 6
#> $ b:List of 2
#> ..$ c: chr "x"
#> ..$ d:List of 2
#> .. ..$ e: num 9
#> .. ..$ f: chr "y"
# Extra arguments are passed on to f.
nested_titles <- list(
shelf = list(" the quiet streets ", "THE LONG TABLE"),
other = " Paper Moons"
)
str(rapply(nested_titles, trimws, how = "replace"))
#> List of 2
#> $ shelf:List of 2
#> ..$ : chr "the quiet streets"
#> ..$ : chr "THE LONG TABLE"
#> $ other: chr "Paper Moons"
str(rapply(nested_titles, \(s, ...) tolower(trimws(s, ...)),
which = "left", how = "replace"))
#> List of 2
#> $ shelf:List of 2
#> ..$ : chr "the quiet streets "
#> ..$ : chr "the long table"
#> $ other: chr "paper moons"
Source: R documentation: rapply
A caveat is that rapply() sees only the leaves, so it cannot be used to change the lists themselves, and a data frame, which is itself a list, is recursed into: its columns are the leaves. For work on whole records, a recursive function of your own that calls lapply() is the tool; the page The apply Family and Functionals covers lapply(), vapply(), Map() and the rest.
Inspecting with str()
str() is the first function to call on an unfamiliar list. Its help page describes it as compactly displaying the internal structure of an R object, a diagnostic function and an alternative to summary(), and as especially well suited to compactly display the (abbreviated) contents of possibly nested lists. It writes one line per element, with a $ name: prefix, the type, the length in brackets when it is not one, and the first values; nested lists are indented with ...
# str() shows the structure of any object compactly.
entry <- list(
title = "The Dispossessed",
year = 1974L,
genres = c("Science fiction", "Classic"),
edition = list(number = 2L, notes = list("map", "index"))
)
str(entry)
#> List of 4
#> $ title : chr "The Dispossessed"
#> $ year : int 1974
#> $ genres : chr [1:2] "Science fiction" "Classic"
#> $ edition:List of 2
#> ..$ number: int 2
#> ..$ notes :List of 2
#> .. ..$ : chr "map"
#> .. ..$ : chr "index"
# max.level limits how deep it goes.
str(entry, max.level = 1L)
#> List of 4
#> $ title : chr "The Dispossessed"
#> $ year : int 1974
#> $ genres : chr [1:2] "Science fiction" "Classic"
#> $ edition:List of 2
str(entry, max.level = 2L)
#> List of 4
#> $ title : chr "The Dispossessed"
#> $ year : int 1974
#> $ genres : chr [1:2] "Science fiction" "Classic"
#> $ edition:List of 2
#> ..$ number: int 2
#> ..$ notes :List of 2
Source: R documentation: str
The arguments of str() to know are the following; the defaults are in the str component of options() and are set together with strOptions().
| Argument | Effect |
|---|---|
|
the deepest level of nesting to show; the default |
|
how many of the first elements of each vector to show; the default is 4, and the number is scaled by a factor that depends on the kind of vector |
|
whether attributes are shown as sub-structures ( |
|
whether the length is shown as |
|
the maximum number of list elements to show (99 by default), after which |
|
the maximum number of characters shown for a string (128 by default) |
|
whether the type and length are shown at all |
Source: R documentation: str
# vec.len controls how many elements of each vector are shown; give.attr
# hides attributes; give.length hides the [1:n] part.
str(1:100)
#> int [1:100] 1 2 3 4 5 6 7 8 9 10 ...
str(1:100, vec.len = 2)
#> int [1:100] 1 2 3 4 5 ...
str(structure(1:3, units = "cm"))
#> int [1:3] 1 2 3
#> - attr(*, "units")= chr "cm"
str(structure(1:3, units = "cm"), give.attr = FALSE)
#> int [1:3] 1 2 3
str(entry, give.length = FALSE, max.level = 1L)
#> List of 4
#> $ title : chr "The Dispossessed"
#> $ year : int 1974
#> $ genres : chr "Science fiction" "Classic"
#> $ edition:List of 2
# str() on other structures.
str(NULL)
#> NULL
str(list())
#> list()
str(sum)
#> function (..., na.rm = FALSE)
str(factor(c("a", "b", "a")))
#> Factor w/ 2 levels "a","b": 1 2 1
str(as.Date("2026-06-30") - as.Date("2026-06-01"))
#> 'difftime' num 29
#> - attr(*, "units")= chr "days"
str(as.Date("2026-06-30"))
#> Date[1:1], format: "2026-06-30"
str(matrix(1:6, nrow = 2L))
#> int [1:2, 1:3] 1 2 3 4 5 6
# list.len limits how many elements of a long list are listed.
str(as.list(1:100), list.len = 3L)
#> List of 100
#> $ : int 1
#> $ : int 2
#> $ : int 3
#> [list output truncated]
str(setNames(as.list(letters), LETTERS), list.len = 2L)
#> List of 26
#> $ A: chr "a"
#> $ B: chr "b"
#> [list output truncated]
Source: R documentation: str
str() is generic, with methods for data frames and others, and the output format is not a stable interface: scripts that parse it break between versions. The NEWS shows how it evolves: in R 4.6.0 str(x) of a list-like x of a class that has an str() method calling NextMethod() no longer prints a stray List of <...> line from the default method; in R 4.5.3 str(x, give.attr = FALSE) stopped showing attributes for a zero-length Date or POSIXt object (R NEWS: Changes in R 4.6.0 and 4.5.3). The output is for human reading; to test a list in code, use names(), lengths(), identical() and all.equal().
Lists with the functional tools
A list is the natural argument of the higher-order functions of base R, which is why they appear on this page only in a short tour; the page The apply Family and Functionals has the detail, and Function Factories and Higher-Order Functions the design side.
-
lapply(x, f)returns a list of the results, one per element, keeping the names;vapply(x, f, template)returns a vector and checks the type of each result.sapply()guesses whether to simplify, which makes its return type depend on the data and is the reason the examples here usevapply(). -
Filter(f, x)keeps the elements for whichfis true,Map(f, ...)appliesfover several lists in parallel and returns a list (it ismapply(SIMPLIFY = FALSE); the names come from the first argument),Reduce(f, x, init, accumulate)folds the elements into one value (or all the partial values, withaccumulate = TRUE), andPosition()andFind()return the position and the value of the first match. They are described in the help pagefunprog, together withNegate(). -
do.call(what, args)calls a function with its arguments taken from a list. Names in the list become argument names, sodo.call(paste, list("a", "b", sep = "-"))ispaste("a", "b", sep = "-"). It is the way to applyrbind,cbind,corMapto a list of unknown length.
# Functions that work on every element of a list.
loans_per_member <- list(ana = c(3, 5), dev = c(2, 8, 6), li = 4)
lapply(loans_per_member, sum)
#> $ana
#> [1] 8
#>
#> $dev
#> [1] 16
#>
#> $li
#> [1] 4
#>
vapply(loans_per_member, length, integer(1L))
#> ana dev li
#> 2 3 1
Filter(\(v) length(v) > 1L, loans_per_member)
#> $ana
#> [1] 3 5
#>
#> $dev
#> [1] 2 8 6
#>
Map(\(name, v) paste(name, sum(v)), names(loans_per_member), loans_per_member)
#> $ana
#> [1] "ana 8"
#>
#> $dev
#> [1] "dev 16"
#>
#> $li
#> [1] "li 4"
#>
Reduce(\(total, v) total + sum(v), loans_per_member, 0)
#> [1] 28
Reduce(\(total, v) total + sum(v), loans_per_member, 0, accumulate = TRUE)
#> [1] 0 8 24 28
Reduce(c, loans_per_member)
#> [1] 3 5 2 8 6 4
Position(\(v) length(v) == 3L, loans_per_member)
#> [1] 2
Find(\(v) length(v) == 3L, loans_per_member)
#> [1] 2 8 6
# do.call() takes the arguments as a list.
args <- list(1:10, na.rm = TRUE)
do.call(mean, args)
#> [1] 5.5
do.call(paste, list("a", "b", sep = "-"))
#> [1] "a-b"
do.call("sum", list(1, 2, 3))
#> [1] 6
Source: R documentation: funprog
Converting to and from lists
as.list() converts a vector, keeping the names (since R 4.2.0 it keeps the names of a factor, too: R NEWS: Changes in R 4.2.0), and as.list() of an environment returns its bindings as a named list, in no guaranteed order unless sorted = TRUE; list2env() goes the other way. unlist() is the reverse of as.list() for a vector. stack() turns a named list of vectors into a data frame of two columns, values and ind (a factor), and unstack() reverses it; relist(flesh, skeleton) gives a flat vector the shape of a skeleton, which makes unlist() reversible when the skeleton is kept. The NEWS of R 4.3.0 records that relist() now supports NULL elements in the skeleton.
# as.list() on vectors and environments.
as.list(c(a = 1, b = 2))
#> $a
#> [1] 1
#>
#> $b
#> [1] 2
#>
as.list(1:3)
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] 2
#>
#> [[3]]
#> [1] 3
#>
e <- new.env()
assign("title", "Notes on Gravity", envir = e)
assign("year", 2017L, envir = e)
str(as.list(e, sorted = TRUE))
#> List of 2
#> $ title: chr "Notes on Gravity"
#> $ year : int 2017
e <- list2env(list(x = 1, y = 2), envir = e)
sort(ls(e))
#> [1] "title" "x" "y" "year"
# stack() turns a named list of vectors into a data frame.
stacked <- stack(loans_per_member)
stacked
#> values ind
#> 1 3 ana
#> 2 5 ana
#> 3 2 dev
#> 4 8 dev
#> 5 6 dev
#> 6 4 li
class(stacked$ind)
#> [1] "factor"
unstack(stacked)
#> $ana
#> [1] 3 5
#>
#> $dev
#> [1] 2 8 6
#>
#> $li
#> [1] 4
#>
# relist() rebuilds the shape after unlist().
skeleton <- list(a = 1:2, b = list(c = 3, d = 4:5))
flat <- unlist(skeleton)
flat
#> a1 a2 b.c b.d1 b.d2
#> 1 2 3 4 5
str(relist(flat * 10, skeleton))
#> List of 2
#> $ a: num [1:2] 10 20
#> $ b:List of 2
#> ..$ c: num 30
#> ..$ d: num [1:2] 40 50
identical(relist(flat, skeleton), skeleton)
#> [1] FALSE
Source: R documentation: relist
Comparing lists
== does not compare two lists: for two lists it signals comparison of these types is not implemented, and for a list and an atomic vector it compares only when every element has length one, otherwise it fails with 'list' object cannot be coerced to type 'double'. Use identical(x, y) for exact equality, which compares the types, the values, the names and every attribute, and all.equal(x, y) for equality up to numerical tolerance: it returns TRUE or a character vector that says what differs, element by element, including the names and the lengths. Wrap all.equal() in isTRUE() when it is a condition, as the page Operators and Precedence explains for floating-point numbers.
# Comparing lists.
identical(list(1, "a"), list(1, "a"))
#> [1] TRUE
identical(list(1L), list(1))
#> [1] FALSE
all.equal(list(1, "a"), list(1L, "a"))
#> [1] TRUE
all.equal(list(a = 1, b = "x"), list(a = 2, b = "y"))
#> [1] "Component “a”: Mean relative difference: 1"
#> [2] "Component “b”: 1 string mismatch"
all.equal(list(a = 1), list(b = 1))
#> [1] "Names: 1 string mismatch"
all.equal(list(1, 2), list(1))
#> [1] "Length mismatch: comparison on first 1 components"
isTRUE(all.equal(list(a = 1), list(a = 1 + 1e-10)))
#> [1] TRUE
Source: R documentation: all.equal
Lists and other types
A list is not an atomic vector, and the functions for numbers do not accept one. sum(list(1, 2, 3)) is an error, l + 1 is an error, and is.numeric(l) is FALSE even if every element is a number; the fix is unlist() first, or vapply() over the elements. In the other direction, x[2L] <- list("text") on an atomic vector turns the whole vector into a list, with each value in its own element, and $<- on an atomic vector coerces it with the warning Coercing LHS to a list, while [[<- adds an element to the vector without making it a list.
# A list is not interchangeable with an atomic vector.
v <- 1:3
l <- list(1, 2, 3)
identical(v, l)
#> [1] FALSE
identical(as.list(v), l)
#> [1] FALSE
identical(unlist(l), as.numeric(v))
#> [1] TRUE
tryCatch(sum(l), error = \(e) conditionMessage(e))
#> [1] "invalid 'type' (list) of argument"
tryCatch(l + 1, error = \(e) conditionMessage(e))
#> [1] "non-numeric argument to binary operator"
sum(unlist(l))
#> [1] 6
is.numeric(l)
#> [1] FALSE
mean(unlist(l))
#> [1] 2
# Lists compare by identical() or all.equal(), not by ==.
list(1, 2) == 1
#> [1] TRUE FALSE
tryCatch(list(1, 2) == list(1, 2), error = \(e) conditionMessage(e))
#> [1] "comparison of these types is not implemented"
tryCatch(list(1, 2:3) == 1, error = \(e) conditionMessage(e))
#> [1] "'list' object cannot be coerced to type 'double'"
# Assigning into an atomic vector: [[<- adds, $<- coerces to a list.
atomic <- c(a = 1, b = 2)
atomic[["c"]] <- 3
atomic
#> a b c
#> 1 2 3
tryCatch(atomic$d <- 4, warning = \(w) conditionMessage(w))
#> [1] "Coercing LHS to a list"
tryCatch(atomic[["d"]] <- 1:2, error = \(e) conditionMessage(e))
#> [1] "more elements supplied than there are to replace"
atomic[2L] <- list("text")
str(atomic)
#> List of 3
#> $ a: num 1
#> $ b: chr "text"
#> $ c: num 3
# A list assigned into a list element is stored as one element.
store <- list(a = 1)
store["b"] <- list(1:3)
store[["c"]] <- 1:3
tryCatch(store["d"] <- 1:3, warning = \(w) conditionMessage(w))
#> [1] "number of items to replace is not a multiple of replacement length"
lengths(store)
#> a b c
#> 1 3 3
Source: R documentation: Extract
The messages in the block are the ones R 4.6.1 printed: invalid 'type' (list) of argument for sum() of a list, non-numeric argument to binary operator for l + 1, Coercing LHS to a list for $<- on a vector, more elements supplied than there are to replace for a length-two value in [[<- and number of items to replace is not a multiple of replacement length when a vector longer than one is assigned to a list slot with [<- and the value is not a list. The same block shows the safe ways to store a vector in a list: store["b"] <- list(1:3) and store[["c"]] <- 1:3 each put the whole vector into one element, and store["d"] <- 1:3 is the one that warns.
NULL, empty elements and matrices of lists
NULL in a list is a value like any other, with three consequences. list(1, NULL, 3) has three elements, whereas c(1, NULL, 3) has two, because c() drops NULL; lengths() returns 0 for a NULL element and for an empty vector alike; and lapply() keeps a NULL result in its slot, so the output is as long as the input. Filter(Negate(is.null), x) drops the NULL elements (see FAQ 7.1 above for the reverse).
# NULL inside a list: kept by list(), dropped by c().
length(list(1, NULL, 3))
#> [1] 3
length(c(1, NULL, 3))
#> [1] 2
length(c(list(1), list(NULL)))
#> [1] 2
lengths(list(a = NULL, b = integer(), c = 1:2))
#> a b c
#> 0 0 2
Filter(Negate(is.null), list(a = 1, b = NULL, c = 3))
#> $a
#> [1] 1
#>
#> $c
#> [1] 3
#>
vapply(list(1, NULL, 3), is.null, logical(1L))
#> [1] FALSE TRUE FALSE
# lapply() over a list that holds NULL keeps the slot.
str(lapply(list(1, NULL, 3), \(v) v))
#> List of 3
#> $ : num 1
#> $ : NULL
#> $ : num 3
str(lapply(list(a = 1, b = 2), \(v) if (v > 1) NULL else v))
#> List of 2
#> $ a: num 1
#> $ b: NULL
Source: R documentation: NULL
A list can also have a dim attribute and become a matrix or an array whose cells hold anything. A list-matrix is created with matrix(list(...), nrow = 2L), indexed with cells[[i, j]] for the content and with cells[i, j] for a one-cell list. It is a rarely used structure in everyday code; the page Matrices and Arrays covers the general case. A matrix with a list type is not is.numeric, and arithmetic on it fails like it does on a list.
# A list in a matrix: a "list-matrix" holds anything in each cell.
cells <- matrix(list(1, "a", TRUE, NULL, 1:3, sum), nrow = 2L)
dim(cells)
#> [1] 2 3
typeof(cells)
#> [1] "list"
cells[[1L, 2L]]
#> [1] TRUE
cells[2L, 3L]
#> [[1]]
#> function (..., na.rm = FALSE) .Primitive("sum")
#>
cells[[2L, 2L]]
#> NULL
is.list(cells)
#> [1] TRUE
Source: R documentation: matrix
Building data frames from lists with list2DF()
list2DF(x = list(), nrow = 0) creates a data frame from a list of variables. It is a low-level constructor that was added in R 4.0.0: the NEWS of that release says "New function list2DF() for creating data frames from lists of variables", and the NEWS of R 4.2.0 says that list2DF() now checks that its arguments are of the same length, rather than use recycling (R NEWS: Changes in R 4.0.0 and 4.2.0). The help page has a one-line Details section: all list elements are taken "as is". That sentence is the whole difference from data.frame() and as.data.frame():
| Behaviour | list2DF(x) |
data.frame(x) and as.data.frame(x) |
|---|---|---|
Names |
kept exactly ( |
made syntactic by default ( |
Recycling |
none: unequal lengths are an error, |
recycled when the lengths divide, otherwise the error |
List elements |
a list element becomes a list column, one element per row |
a list element is split into several columns by |
Conversion |
none: no factors, no stringsAsFactors, no checks |
the column types are as given in R 4.0.0 and later; |
Unnamed list |
empty names |
names invented from the values ( |
Source: R documentation: list2DF
source("scenario.R")
# list2DF() builds a data frame from a list of equal-length variables.
cols <- list(
title = c("Paper Moons", "Ten Winters", "Loud Roofs"),
year = c(2005L, 2022L, 2021L)
)
shelf <- list2DF(cols)
shelf
#> title year
#> 1 Paper Moons 2005
#> 2 Ten Winters 2022
#> 3 Loud Roofs 2021
str(shelf)
#> 'data.frame': 3 obs. of 2 variables:
#> $ title: chr "Paper Moons" "Ten Winters" "Loud Roofs"
#> $ year : int 2005 2022 2021
class(shelf)
#> [1] "data.frame"
attributes(shelf)
#> $names
#> [1] "title" "year"
#>
#> $class
#> [1] "data.frame"
#>
#> $row.names
#> [1] 1 2 3
#>
# It does no name repair, no conversion and no recycling.
odd <- list2DF(list(`loan id` = 1:2, `2nd` = c("a", "b")))
names(odd)
#> [1] "loan id" "2nd"
names(as.data.frame(list(`loan id` = 1:2, `2nd` = c("a", "b"))))
#> [1] "loan.id" "X2nd"
tryCatch(
list2DF(list(a = 1:3, b = 1:2)),
error = \(e) conditionMessage(e)
)
#> [1] "all variables should have the same length"
tryCatch(
as.data.frame(list(a = 1:3, b = 1:2)),
error = \(e) conditionMessage(e)
)
#> [1] "arguments imply differing number of rows: 3, 2"
Source: R documentation: list2DF
A list column is a column that is a list, one element per row; each element can have any length, which is the use case that the help page example shows (a data frame of a list of character vectors and their lengths). list2DF() is the direct way to build one. A data frame with zero columns and a given number of rows needs nrow. Because it does not check or convert, list2DF() suits a list of columns that are already what they should be, and the price is that it does not protect against a column of the wrong kind. For the data frame itself, see Data Frames.
# A list column keeps list elements, one per row.
with_list <- list2DF(list(
title = c("Paper Moons", "Ten Winters"),
genres = list(c("Poetry", "Classic"), "Fiction")
))
str(with_list)
#> 'data.frame': 2 obs. of 2 variables:
#> $ title : chr "Paper Moons" "Ten Winters"
#> $ genres:List of 2
#> ..$ : chr "Poetry" "Classic"
#> ..$ : chr "Fiction"
with_list$genres[[1L]]
#> [1] "Poetry" "Classic"
nrow(with_list)
#> [1] 2
# Zero columns need nrow; with no arguments it is an empty data frame.
dim(list2DF())
#> [1] 0 0
dim(list2DF(nrow = 3L))
#> [1] 3 0
# Unnamed lists: the names come out empty.
names(list2DF(list(1:2, c("a", "b"))))
#> [1] "" ""
names(as.data.frame(list(1:2, c("a", "b"))))
#> [1] "X1.2" "c..a....b.."
# Comparison with the other constructors on the shelf data.
by_hand <- list2DF(unclass(books)[c("title", "year")])
identical(by_hand, books[c("title", "year")])
#> [1] TRUE
all.equal(by_hand, books[c("title", "year")])
#> [1] TRUE
class(attr(by_hand, "row.names"))
#> [1] "integer"
Source: R documentation: data.frame
Lists as records
A record is a fixed set of named fields, each of which can have its own type: the thing that a struct, a class or a row is in other languages. In R the plain form is a named list. The shelf scenario models one book as exactly that, and the low-level constructor new_book() in scenario.R shows the whole recipe: a list of length-one fields with the attribute class set to "book". The book() helper trims and normalises the values and validates them; the printing method shows <book> title (author, year). Everything that this page showed about lists therefore works on a book: b$title, b[["year"]], names(b), str(b). unclass(b) strips the class and prints the plain list, which is the way to see the fields when a print method hides them; the page S3 Classes and Generics explains unclass(), inherits() and the constructor, validator and helper pattern.
source("scenario.R")
# A book is a list with class "book": see new_book() in scenario.R.
b <- book(" The Dispossessed ", "Ursula K. Le Guin", "978-0-306-40615-7",
1974, "Science fiction", 387)
typeof(b)
#> [1] "list"
class(b)
#> [1] "book"
is.list(b)
#> [1] TRUE
names(b)
#> [1] "title" "author" "isbn" "year" "genre" "pages"
b$title
#> [1] "The Dispossessed"
b[["year"]]
#> [1] 1974
unclass(b)
#> $title
#> [1] "The Dispossessed"
#>
#> $author
#> [1] "Ursula K. Le Guin"
#>
#> $isbn
#> [1] "9780306406157"
#>
#> $year
#> [1] 1974
#>
#> $genre
#> [1] "Science fiction"
#>
#> $pages
#> [1] 387
#>
str(b)
#> List of 6
#> $ title : chr "The Dispossessed"
#> $ author: chr "Ursula K. Le Guin"
#> $ isbn : chr "9780306406157"
#> $ year : int 1974
#> $ genre : chr "Science fiction"
#> $ pages : int 387
#> - attr(*, "class")= chr "book"
b
#> <book> The Dispossessed (Ursula K. Le Guin, 1974)
Source: R documentation: class (unclass)
The price of a list is that nothing protects the invariants: $<- on a list with a class is the ordinary $<- of a list, because the class has no $<- method, so a field can be set to any value at any time and the validation of book() is bypassed. The block below sets the year to 3000, which validate_book() rejects with implausible year: 3000, and the assignment happens anyway. This is the trade-off that S4, Reference Classes, R6 and S7 exist to change (see OOP Overview and Base Types); in S3 the remedy is to treat the list as read-only by convention and to build a new object with the helper, or to write a replacement method.
# Because it is a list, a field can be replaced without validation.
b$year <- 3000L
b$year
#> [1] 3000
tryCatch(validate_book(b), error = \(e) conditionMessage(e))
#> [1] "implausible year: 3000"
Source: R documentation: structure
A record without a class, and a list of records
A named list without a class is the lightest record: list(title = "Paper Moons", author = "Noor Haddad", year = 2005L). structure(rec, class = "book") gives it the class afterwards (it sets the attribute, no validation is done), and format(rec_book) then dispatches to the method of the class. Because the fields are elements, a subset of the fields is a [ away: rec[c("title", "year")] is a record with two fields.
# A record in plain list form, and conversion to and from one.
rec <- list(title = "Paper Moons", author = "Noor Haddad", year = 2005L)
rec$author
#> [1] "Noor Haddad"
rec[c("title", "year")]
#> $title
#> [1] "Paper Moons"
#>
#> $year
#> [1] 2005
#>
rec_book <- structure(rec, class = "book")
format(rec_book)
#> [1] "Paper Moons (Noor Haddad, 2005)"
Source: R documentation: structure
Records are most useful in a collection, and the collection is a list of lists. The block below builds a list of eight book objects from the first eight rows of the books data frame (the ninth and the thirteenth rows have an empty and a wrong ISBN, which book() rejects, which is the point of a validated constructor). The results show the idioms that replace a loop: lapply() with an anonymous function over the list returns a list of one field per record, vapply() returns the same as a vector of a declared type, Filter() keeps the records that satisfy a condition, and sort() of the vector of fields sorts the titles. A list of books prints through the print method of each element, with the position labels [[1]] and [[2]] above them, as the shelf_books[2:3] output shows.
# A list of books.
first <- head(books, 8L)
shelf_books <- lapply(seq_len(nrow(first)), \(i) {
row <- first[i, ]
book(row$title, row$author, row$isbn, row$year, row$genre, row$pages)
})
length(shelf_books)
#> [1] 8
shelf_books[[1L]]
#> <book> The Dispossessed (Ursula K. Le Guin, 1974)
shelf_books[2:3]
#> [[1]]
#> <book> Pride and Prejudice (Jane Austen, 1813)
#>
#> [[2]]
#> <book> the quiet streets (Mara Quill, 2019)
#>
vapply(shelf_books, \(b) b$title, character(1L))
#> [1] "The Dispossessed" "Pride and Prejudice" "the quiet streets"
#> [4] "Loud Roofs" "Midnight Ledger" "A Map of Salt"
#> [7] "Garden of Small Fires" "The Lamp at Noon"
lapply(shelf_books[1:2], \(b) b$title)
#> [[1]]
#> [1] "The Dispossessed"
#>
#> [[2]]
#> [1] "Pride and Prejudice"
#>
Filter(\(b) !is.na(b$year) && b$year < 2000L, shelf_books) |>
vapply(format, character(1L))
#> [1] "The Dispossessed (Ursula K. Le Guin, 1974)"
#> [2] "Pride and Prejudice (Jane Austen, 1813)"
#> [3] "The Lamp at Noon (Le Guin, Ursula K., 1999)"
sort(vapply(shelf_books, \(b) b$title, character(1L)))
#> [1] "A Map of Salt" "Garden of Small Fires" "Loud Roofs"
#> [4] "Midnight Ledger" "Pride and Prejudice" "The Dispossessed"
#> [7] "The Lamp at Noon" "the quiet streets"
shelf_books[[7L]]$year
#> [1] NA
Source: R documentation: lapply
Two details of the output are not accidents. The title of the third book, the quiet streets, is lower case because book() trims the blanks but does not change case, and the record of the seventh book, Garden of Small Fires, has NA for its year, which is why the Filter() condition tests !is.na(b$year) first. The field of a missing value stays in the record as an NA of the right type and is not removed, and that is what keeps the record the same shape for every book.
The way back from a list of records to a table is do.call(rbind, lapply(records, as.data.frame)): the inner call turns each record into a one-row data frame (after unclass() so that the book class does not interfere), and rbind() stacks them. It is the standard idiom and it is slow for thousands of rows, because it makes one data frame per record; the faster form is to build one vector per field with vapply() and use list2DF() or data.frame() on the list of columns, as in the section on JSON-shaped lists below.
# Back to a data frame: one row per record.
rows <- lapply(shelf_books, \(b) as.data.frame(unclass(b)))
do.call(rbind, rows)[, c("title", "year", "pages")]
#> title year pages
#> 1 The Dispossessed 1974 387
#> 2 Pride and Prejudice 1813 432
#> 3 the quiet streets 2019 214
#> 4 Loud Roofs 2021 198
#> 5 Midnight Ledger 2016 301
#> 6 A Map of Salt 2008 NA
#> 7 Garden of Small Fires NA 96
#> 8 The Lamp at Noon 1999 280
Source: R documentation: cbind (rbind)
Lists as the basis of S3 objects
An S3 object is nothing more than a base-type object with a class attribute, and the base type is usually a list. The book is one example; a fitted model from lm() is another, a list of coefficients, residuals and other components of class "lm"; so is the result of t.test(), of class "htest". The consequence is that you can open an S3 object with the list tools: names(fit), fit$coefficients, str(unclass(fit)). The R Language Definition describes the mechanism in the section on object-oriented programming: the class is the attribute, and the dispatch looks at it (The R Language Definition: Object-oriented programming). The practice that follows from the list base is simple:
-
Build the object with
structure(list(...), class = "name")inside a constructor function that checks the types, and validate the values in a separate function, as the scenario does withnew_book()andvalidate_book(). -
Access the fields with
$and[[inside the methods, and give the users functions (accessors) so that the user code does not depend on the layout. -
Keep in mind that
unclass(x)removes the class and thatinherits(x, "book")tests it; both are covered with the dispatch rules in S3 Classes and Generics.
Lists as the basis of data frames
A data frame is a list. More exactly, it is a list of equal-length columns with the attribute class set to "data.frame", the attribute names (the column names) and the attribute row.names. An Introduction to R says it at the start of its section on data frames: a data frame is a list with class "data.frame", with restrictions on the lists that may be made into data frames; vector variables must all have the same length, and matrix variables the same number of rows (An Introduction to R: Data frames). Every list operation of this page therefore works on a data frame, and the data frame class changes only the operators that need the row dimension.
Expression on books |
Read as a list | Read as a data frame |
|---|---|---|
|
the number of elements: 6 |
the number of columns, not the number of rows |
|
the second element |
the second column, as a vector |
|
a sub-list of one element |
a data frame of one column (the single-index |
|
not a list operation |
the comma form is the matrix-like indexing of the data frame: a column vector, or three rows |
|
applies to each element |
applies to each column |
|
deletes the element |
deletes the column |
|
the element names |
the column names |
The manual says it directly for the data frame method of [: if two indices are supplied (even if one is empty), it creates matrix-like indexing for a structure that is basically a list of vectors of the same length, and if a single index is supplied, it is interpreted as indexing the list of columns; in that case the drop argument is ignored, with a warning. The next block looks at books as a list: its type is "list", its length is the number of columns, lengths() is the number of rows repeated for each column, and vapply(books, typeof, character(1L)) is the schema in one line.
source("scenario.R")
# A data frame is a list of equal-length columns with a class attribute.
typeof(books)
#> [1] "list"
class(books)
#> [1] "data.frame"
is.list(books)
#> [1] TRUE
is.vector(books)
#> [1] FALSE
is.recursive(books)
#> [1] TRUE
length(books)
#> [1] 6
names(books)
#> [1] "title" "author" "isbn" "year" "genre" "pages"
lengths(books)
#> title author isbn year genre pages
#> 16 16 16 16 16 16
vapply(books, typeof, character(1L))
#> title author isbn year genre pages
#> "character" "character" "character" "integer" "character" "integer"
vapply(books, class, character(1L))
#> title author isbn year genre pages
#> "character" "character" "character" "integer" "character" "integer"
# [[ extracts a column; [ keeps a data frame; [ with a comma gives rows.
class(books[[2L]])
#> [1] "character"
class(books[2L])
#> [1] "data.frame"
class(books[, 2L])
#> [1] "character"
head(books[["author"]], 3L)
#> [1] "Ursula K. Le Guin" "Jane Austen" "Mara Quill"
books[["year"]][1:3]
#> [1] 1974 1813 2019
Source: R documentation: data.frame
attributes(books) is the full list of what makes the list a data frame. The row names print as the integers 1:16, but they are stored in a compact form that .row_names_info() reveals: a negative number, here -16, means the row names are automatic and the number is minus the number of rows, and type = 2L gives the absolute count. attributes() expands the compact form for printing, which is why the output above shows the integers 1 to 16.
# The attributes are names, class and row.names.
attributes(books)
#> $names
#> [1] "title" "author" "isbn" "year" "genre" "pages"
#>
#> $class
#> [1] "data.frame"
#>
#> $row.names
#> [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#>
.row_names_info(books)
#> [1] -16
.row_names_info(books, type = 2L)
#> [1] 16
# unclass() shows the list underneath.
str(unclass(books[1:2, c("title", "year")]))
#> List of 2
#> $ title: chr [1:2] "The Dispossessed" "Pride and Prejudice"
#> $ year : int [1:2] 1974 1813
#> - attr(*, "row.names")= int [1:2] 1 2
unclass() removes the class and shows the underlying list with its row.names attribute, which is how a data frame looks to the code that does not know about the class. as.list() returns a plain list of the columns and drops the data frame class and the row names. The last lines of the next block use the list view for column work: Filter(is.numeric, books) selects the numeric columns, as a data frame because Filter() indexes with [, and vapply() over them computes a summary per column. Deleting an element by NULL works on a data frame as on a list (books_copy[["pages"]] <- NULL), which is why the number of columns in books_copy drops with each deletion and books itself is not changed.
# as.list() and list-style operations.
str(as.list(books[1:2, c("title", "year")]))
#> List of 2
#> $ title: chr [1:2] "The Dispossessed" "Pride and Prejudice"
#> $ year : int [1:2] 1974 1813
books_copy <- books
books_copy[["pages"]] <- NULL
names(books_copy)
#> [1] "title" "author" "isbn" "year" "genre"
books_copy$year <- NULL
ncol(books_copy)
#> [1] 4
Filter(is.numeric, books) |> names()
#> [1] "year" "pages"
vapply(Filter(is.numeric, books), \(col) sum(col, na.rm = TRUE), numeric(1L))
#> year pages
#> 29981 3755
# Building one from a list of columns.
cols <- list(title = c("Paper Moons", "Ten Winters"), year = c(2005L, 2022L))
list2DF(cols)
#> title year
#> 1 Paper Moons 2005
#> 2 Ten Winters 2022
as.data.frame(cols)
#> title year
#> 1 Paper Moons 2005
#> 2 Ten Winters 2022
data.frame(cols)
#> title year
#> 1 Paper Moons 2005
#> 2 Ten Winters 2022
Source: R documentation: as.data.frame
split() goes the other way round: it returns a list, one element per group, and a list of data frames or vectors is the usual intermediate result of grouped work (the page Data Frames and the apply family page build on it). The names of the result are the levels of the grouping variable, as pages_by_genre shows, and a grouping with an NA drops those rows.
# split() returns a list: one element per group.
pages_by_genre <- split(books$pages, tolower(trimws(books$genre)))
lengths(pages_by_genre)
#> classic fiction mystery non-fiction poetry
#> 2 4 2 3 2
#> science fiction
#> 3
vapply(pages_by_genre, \(p) sum(p, na.rm = TRUE), numeric(1L))
#> classic fiction mystery non-fiction poetry
#> 712 668 545 527 184
#> science fiction
#> 1119
str(split(books[c("title", "year")], books$year > 2015)[["TRUE"]])
#> 'data.frame': 8 obs. of 2 variables:
#> $ title: chr " the quiet streets " "Loud Roofs" "Midnight Ledger" "THE LONG TABLE" ...
#> $ year : int 2019 2021 2016 2023 2017 2020 2022 2018
Source: R documentation: split
JSON-shaped nested lists
A nested list is also the natural R image of a JSON document. A JSON object becomes a named list, a JSON array becomes either an atomic vector (when all its values have one scalar type) or an unnamed list, JSON null becomes NULL, and the three scalar types become length-one logical, numeric and character vectors. The package jsonlite (on CRAN; it is the usual tool and is not used here) reads and writes this mapping, with arguments such as simplifyVector and simplifyDataFrame that decide whether arrays of values become vectors and arrays of objects become data frames, or stay lists; the page Serialization: RDS, RData and JSON covers it, and the page HTTP and Web APIs shows how a response body arrives as such a list. The block below builds the list by hand so that the tools of this page can be applied to it without the package.
The shelf catalogue is a named list with a name, an array of three book objects and a flag. str(catalogue, max.level = 2L) shows the top of the tree. The three tags fields are different on purpose: a vector of two strings, an empty character vector (JSON []) and a single string; and the third book has year = NULL, which list() keeps as an element (JSON null) while an assignment b$year <- NULL would delete it.
# A nested list shaped like a JSON document: objects are named lists, arrays
# are unnamed lists or vectors, null is NULL.
catalogue <- list(
shelf = "Main hall",
books = list(
list(title = "Paper Moons", year = 2005L, tags = c("poetry", "short")),
list(title = "Ten Winters", year = 2022L, tags = character()),
list(title = "Loud Roofs", year = NULL, tags = "novel")
),
open = TRUE
)
str(catalogue, max.level = 2L)
#> List of 3
#> $ shelf: chr "Main hall"
#> $ books:List of 3
#> ..$ :List of 3
#> ..$ :List of 3
#> ..$ :List of 3
#> $ open : logi TRUE
# Walk it with [[ and the usual list tools.
catalogue$books[[1L]]$title
#> [1] "Paper Moons"
vapply(catalogue$books, \(b) b$title, character(1L))
#> [1] "Paper Moons" "Ten Winters" "Loud Roofs"
vapply(catalogue$books, \(b) length(b$tags), integer(1L))
#> [1] 2 0 1
vapply(catalogue$books, \(b) is.null(b$year), logical(1L))
#> [1] FALSE FALSE TRUE
# Missing and NULL fields are the same thing to $.
vapply(catalogue$books, \(b) b$year %||% NA_integer_, integer(1L))
#> [1] 2005 2022 NA
names(catalogue$books[[3L]])
#> [1] "title" "year" "tags"
# Flatten to a single named vector, or to a table of the scalar fields.
unlist(catalogue$books[[1L]])
#> title year tags1 tags2
#> "Paper Moons" "2005" "poetry" "short"
rapply(catalogue, \(x) x, classes = "integer", how = "unlist")
#> books.year books.year
#> 2005 2022
rows <- lapply(catalogue$books, \(b) {
list(title = b$title, year = b$year %||% NA_integer_)
})
list2DF(list(
title = vapply(rows, \(r) r$title, character(1L)),
year = vapply(rows, \(r) r$year, integer(1L))
))
#> title year
#> 1 Paper Moons 2005
#> 2 Ten Winters 2022
#> 3 Loud Roofs NA
Source: An Introduction to R: Lists
The block shows the three habits that a JSON-shaped list needs. First, read the fields with vapply() and a declared type, so that a missing or wrongly typed field is an error and not a silent NA. Second, give the missing value an explicit default with the operator %||%, which returns its right-hand side when the left is NULL; it is in base R since 4.4.0 (R NEWS: Changes in R 4.4.0), and the page Operators and Precedence covers it. Third, flatten to a table with list2DF() from vectors that were built field by field, as the last block does, because unlist() of a record turns every field into a string when one of them is a string.
Common errors and warnings
| Message | Code | Fix |
|---|---|---|
|
|
check |
|
|
use |
|
|
find the variable that was meant, a missing assignment is the usual reason |
|
|
|
|
|
use |
|
|
an empty index; classed error |
|
|
build the list with |
|
|
|
|
|
wrap in |
|
|
|
|
|
apply the operator to the elements, with |
|
|
|
|
|
the second argument must be a list |
|
|
make the columns the same length, or use |
Source: R documentation: Extract
Version notes
| Version | Change |
|---|---|
R 3.0.2 |
|
R 3.2.0 |
|
R 3.6.0 |
|
R 4.0.0 |
|
R 4.1.0 |
|
R 4.2.0 |
|
R 4.3.0 |
|
R 4.4.0 |
|
R 4.5.3 |
|
R 4.6.0 |
|
Where older tutorials differ
| Older tutorial | Current practice | Why | Version |
|---|---|---|---|
|
|
|
always |
|
the result is a list |
the help page for the operators and the NEWS describe the change of R 4.0.0 |
R 4.0.0 |
|
|
|
R 4.0.0 |
|
|
one data frame per record is slow; the columns can be built without them |
always |
|
|
a |
always |
|
|
|
always |
|
|
every |
always |
|
|
|
always |
|
|
|
always |
|
|
|
always |
Source: R NEWS: Changes in R 4.0.0
Where to go next
-
Atomic Vectors and Types for the flat vectors that lists hold and the coercion rules
unlist()follows. -
Subsetting and Indexing for the indexing rules of
[,[[and$on every structure. -
Attributes and Names for
names(),attributes()and the other attributes a list can carry. -
Missing and Special Values for
NULL,NAand how they act in lists. -
Data Frames for the class that makes a list a table.
-
The apply Family and Functionals for
lapply(),vapply(),Map(),Filter()andReduce(). -
S3 Classes and Generics for lists with a class.
-
Serialization: RDS, RData and JSON for JSON-shaped lists.
References
-
The R Language Definition: Lists
-
The R Language Definition: Indexing
-
The R Language Definition: Subset assignment
-
An Introduction to R: Lists and data frames
-
An Introduction to R: Constructing and modifying lists
-
R FAQ 7.1: How can I set components of a list to NULL?
-
R NEWS: Changes in R 4.0.0, 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.3 and 4.6.0
-
R NEWS (R 3.x): Changes in R 3.0.2, 3.2.0 and 3.6.0