purrr compact() in R: Drop NULL and Empty List Elements
The purrr compact() function removes NULL and empty (length-zero) elements from a list in a single call, leaving only the entries that hold real values. It is the cleanest way to tidy up the gappy lists that map() and lookups so often produce.
compact(x) # drop NULL and length-0 elements compact(list(a = 1, b = NULL)) # keeps only a compact(map(ids, lookup_fn)) # clean NULLs after a map() compact(x, ~ .x[.x > 0]) # drop by computed emptiness compact(x) |> length() # count surviving elements discard(x, is.na) # NA is NOT empty: use discard
Need explanation? Read on for examples and pitfalls.
What purrr compact() does
compact() is a list pruner. You hand it a list, and it returns a new list with every NULL element and every length-zero element removed. Nothing else changes: surviving elements keep their names, their order, and their values. The target keyword here, purrr compact, describes exactly one job done well.
An element counts as "empty" when its length is zero. That includes NULL, integer(0), character(0), and the empty list list(). A scalar like 5, a string "hi", or even NA all have length one, so compact() keeps them.
NA survives compact() but a NULL does not: NA is a value of length one, while NULL has length zero.compact() syntax and arguments
The signature has two arguments. Only the first is required.
With the default .p = identity, compact() tests each element as-is. When you pass a function to .p, compact() applies it to every element first, then drops the element if the result has length zero. The original value (not the transformed one) is what stays in the output.
compact() examples by use case
Example 1: drop NULL elements from a named list. This is the everyday case. NULL entries vanish, names are preserved.
Example 2: drop empty length-zero elements. Empty vectors are removed alongside NULL, because both have length zero.
Example 3: clean up the output of map(). A lookup that misses returns NULL, so map() over missing keys produces a gappy list. compact() removes the gaps.
Example 4: drop by computed emptiness with a predicate. Pass .p a function. compact() keeps an element only when applying .p to it yields something non-empty.
Here low and mid have no values above 7, so the predicate returns an empty vector and those elements are dropped. high survives, and note it keeps its original value c(9, 10), not the filtered 9 10.
compact() vs discard(): which list cleaner to use
compact() is the specialist; discard() is the generalist. compact() only ever asks "is this empty?" discard() drops elements whenever a predicate you supply returns TRUE, so it can express any rule at all.
| Function | Drops elements when... | Use it for |
|---|---|---|
compact(x) |
element has length zero (NULL, empty) | clearing NULLs and empty results |
discard(x, p) |
predicate p returns TRUE |
any custom condition (NA, negative, too short) |
keep(x, p) |
predicate p returns FALSE |
the inverse of discard() |
Filter(f, x) |
function f returns FALSE |
base R, no purrr dependency |
Decision rule: if you are removing NULL or empty entries, reach for compact() because it reads clearly and needs no predicate. For anything that depends on the value of an element (drop NAs, drop negatives), use discard() instead.
discard(x, ~ length(.x) == 0). They are interchangeable for the NULL-and-empty case, but compact(x) states the intent in one word, which makes pipelines easier to skim.Common pitfalls
Pitfall 1: expecting compact() to remove NA. NA is a value of length one, so compact() keeps it. This silently leaves NAs in a list you thought was clean.
Pitfall 2: assuming compact() recurses into nested lists. compact() only inspects the top level. A NULL buried inside a sub-list is left untouched.
Pitfall 3: using compact() on an atomic vector. Each scalar in an atomic vector has length one, so compact() finds nothing to drop and returns the vector unchanged. compact() is built for lists.
Try it yourself
Try it: Use compact() to remove the NULL and empty elements from the list below. Save the cleaned list to ex_clean.
Click to reveal solution
Explanation: q is NULL and s is integer(0), both length zero, so compact() drops them. p and r hold real values and survive with their names intact.
Related purrr functions
These functions pair naturally with compact() when reshaping or filtering lists:
- discard() drops elements by any predicate you supply.
- keep() keeps elements that pass a predicate, the inverse of discard().
- map() transforms every element and is the usual source of the NULLs that compact() cleans up.
list_flatten()removes one level of nesting from a list.- See the full purrr reference for the predicate-function family.
FAQ
Does purrr compact() remove NA values?
No. compact() removes elements based on length, and NA has length one, so it is treated as a real value and kept. If you need to drop NA, use discard(x, ~ length(.x) == 1 && is.na(.x)), or discard(x, is.na) when every element is a single value. Mixing the two tools is common: run compact() first to clear NULLs, then discard() to clear NAs.
What is the difference between compact() and discard()?
compact() drops only NULL and length-zero elements; it takes no condition because the condition is fixed. discard() drops elements whenever a predicate function you pass returns TRUE, so it can express any rule. In short, compact(x) is a readable shorthand for discard(x, ~ length(.x) == 0). Use compact() for emptiness, discard() for everything else.
Does compact() work on data frames?
A data frame is a list of column vectors, and compact() will treat it that way, dropping any column of length zero. In a valid data frame every column has the same length as the number of rows, so nothing is dropped and the result comes back as a plain list, not a data frame. compact() is meant for ordinary lists, not data frames.
What does compact() return when every element is removed?
It returns an empty list, list(), of length zero. compact() never returns NULL itself, so you can safely pipe its result into length(), map(), or another compact() call without a special case for the all-empty input.
Is compact() recursive?
No. compact() inspects only the top level of the list. A NULL or empty element nested inside a sub-list is left in place. To clean nested lists, apply compact() at each level, for example with modify() or map(), before compacting the outer list.