keep(), discard(), and List Filtering
Filter list elements by predicate functions for clean pipelines.
keep(), discard(), and List Filtering is a free R Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Filtering Lists with purrr
purrr provides a family of functions for filtering, searching, and testing lists based on predicates. A predicate is a function that returns TRUE or FALSE. These functions replace verbose Filter() calls and conditional loops.
library(purrr)
# A mixed list to filter
mixed <- list(1L, 'hello', 3.14, NULL, TRUE, 42L, 'world', NULL)
cat('Total elements:', length(mixed), '\n')
cat('Non-NULL elements:', length(compact(mixed)))keep() — Keep Elements Matching a Predicate
keep(.x, .p) keeps only elements of .x for which the predicate .p returns TRUE. It is the opposite of discard(). The predicate can be a function name, a formula, or a logical vector.
library(purrr)
mixed <- list(1L, 'hello', 3.14, TRUE, 42L, 'world', 2.7)
# Keep only numeric values
numeric_only <- keep(mixed, is.numeric)
print(numeric_only)
# Keep values greater than 5
keep(mixed, ~is.numeric(.x) && .x > 5)discard() — Remove Elements Matching a Predicate
discard(.x, .p) removes elements where the predicate returns TRUE, keeping the rest. It is the mirror of keep(). Use it to filter out nulls, NAs, or unwanted types from a list.
library(purrr)
results <- list(
model_a = list(rmse=2.5, converged=TRUE),
model_b = list(rmse=NA, converged=FALSE),
model_c = list(rmse=1.8, converged=TRUE),
model_d = list(rmse=NA, converged=FALSE)
)
# Discard models that did not converge
converged_models <- discard(results, ~!.x$converged)
map_dbl(converged_models, ~.x$rmse)compact() — Remove NULL Elements
compact(.x) removes all NULL elements from a list — equivalent to discard(.x, is.null) but more concise. It's particularly useful after map() calls that might return NULL for some inputs.
library(purrr)
# Simulate fetching data that might return NULL
ids <- 1:6
fetch_data <- function(id) {
if (id %% 2 == 0) NULL else list(id=id, value=id*10)
}
raw_results <- map(ids, fetch_data)
cat('With NULLs:', length(raw_results), '\n')
clean_results <- compact(raw_results)
cat('Without NULLs:', length(clean_results))detect() — Find First Match
detect(.x, .p) returns the first element that satisfies the predicate. If no element matches, it returns NULL. Use detect(.right=TRUE) to search from the right and find the last match.
library(purrr)
prices <- list(15.99, 8.50, 23.99, 5.25, 19.99, 4.75)
# Find first price under $10
first_cheap <- detect(prices, ~.x < 10)
cat('First price under $10:', first_cheap, '\n')
# Find last price under $10
last_cheap <- detect(prices, ~.x < 10, .right=TRUE)
cat('Last price under $10:', last_cheap)detect_index() — Find Position of First Match
detect_index(.x, .p) returns the index of the first element satisfying the predicate (or 0L if none found). Useful when you need the position, not the value — for example, to update or extract that element.
library(purrr)
models <- list(
list(name='lm', rmse=3.5),
list(name='rf', rmse=2.1),
list(name='xgb', rmse=1.8),
list(name='svm', rmse=2.4)
)
# Find the index of the best model (lowest RMSE)
best_idx <- detect_index(models, function(m) m$rmse < 2.0)
cat('Best model index:', best_idx, '\n')
cat('Best model name:', models[[best_idx]]$name)every() — Do All Elements Match?
every(.x, .p) returns TRUE if the predicate is satisfied for every element. Short-circuits on the first FALSE. Equivalent to all(map_lgl(.x, .p)) but lazily evaluated.
library(purrr)
batch_results <- list(
list(status='success', rows=150),
list(status='success', rows=200),
list(status='success', rows=175)
)
failed_batch <- list(
list(status='success', rows=150),
list(status='error', rows=0),
list(status='success', rows=175)
)
cat('All success (good):', every(batch_results, ~.x$status == 'success'), '\n')
cat('All success (bad):', every(failed_batch, ~.x$status == 'success'))some() — Do Any Elements Match?
some(.x, .p) returns TRUE if at least one element satisfies the predicate. Short-circuits on the first TRUE. Equivalent to any(map_lgl(.x, .p)) but lazily evaluated.
library(purrr)
datasets <- list(
data.frame(x=1:3, y=c(1,NA,3)),
data.frame(x=4:6, y=7:9),
data.frame(x=7:9, y=c(NA,NA,NA))
)
# Does any dataset contain NAs?
cat('Any NAs:', some(datasets, anyNA), '\n')
# Is every dataset NA-free?
cat('All clean:', every(datasets, ~!anyNA(.x)))none() — Do No Elements Match?
none(.x, .p) returns TRUE if no element satisfies the predicate. It's equivalent to !some(.x, .p) but reads more clearly in validation contexts where you want to assert the absence of a condition.
library(purrr)
api_responses <- list(
list(code=200, data='OK'),
list(code=200, data='OK'),
list(code=201, data='Created')
)
# Ensure no errors occurred
no_errors <- none(api_responses, ~.x$code >= 400)
cat('No errors:', no_errors, '\n')
# Check another batch
bad_batch <- c(api_responses, list(list(code=500, data='Error')))
none(bad_batch, ~.x$code >= 400)Combining keep() with map()
Chaining keep() (or discard()) with map() is a common pattern: first filter the list to relevant elements, then apply a transformation. This separates filtering logic from processing logic cleanly.
library(purrr)
model_results <- list(
list(name='A', converged=TRUE, rmse=2.5),
list(name='B', converged=FALSE, rmse=NA),
list(name='C', converged=TRUE, rmse=1.8),
list(name='D', converged=TRUE, rmse=3.1)
)
# Keep converged, extract RMSE
model_results %>%
keep(~.x$converged) %>%
map_dbl(~.x$rmse)Using keep() with is.* Functions
R's built-in is.* predicate functions (is.numeric, is.character, is.null, is.na, is.finite) work directly as predicates in keep() and discard() without needing ~ wrapping.
library(purrr)
mixed_env <- list(
count = 42L,
name = 'Alice',
ratio = 3.14,
flag = TRUE,
missing = NULL,
data = data.frame(x=1:3)
)
# Extract different types
numeric_items <- keep(mixed_env, is.numeric)
cat('Numeric items:', length(numeric_items), '\n')
# Remove NULL items
cleaned <- discard(mixed_env, is.null)
cat('After removing NULL:', length(cleaned))Quick Check
What does detect(.x, .p) return when no element in .x satisfies predicate .p?
Recap: List Filtering Functions
Key takeaways for purrr's list filtering tools:
keep(.x, .p)— keep elements where predicate is TRUEdiscard(.x, .p)— remove elements where predicate is TRUEcompact(.x)— remove all NULL elementsdetect(.x, .p)— return first element matching predicate (or NULL)detect_index(.x, .p)— return index of first match (or 0L)every(.x, .p)— TRUE if all elements matchsome(.x, .p)— TRUE if any element matchesnone(.x, .p)— TRUE if no element matches
library(purrr)
scores <- list(45, 72, 88, 53, 91, 67, 39, 85)
passing <- keep(scores, ~.x >= 60)
failing <- discard(scores, ~.x >= 60)
cat('Passing:', length(passing), 'students\n')
cat('Failing:', length(failing), 'students\n')
cat('All pass:', every(scores, ~.x >= 60), '\n')
cat('Any fail:', some(scores, ~.x < 60), '\n')
cat('First pass score:', detect(scores, ~.x >= 60))Frequently asked questions
Is the “keep(), discard(), and List Filtering” lesson free?
Yes — the full text of “keep(), discard(), and List Filtering” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.
What will I learn in “keep(), discard(), and List Filtering”?
Filter list elements by predicate functions for clean pipelines. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start R Academy?
No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “keep(), discard(), and List Filtering” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this R Academy lesson?
Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- map() and Typed Variants
- map2() and pmap() for Multiple Inputs
- reduce(), accumulate(), and walk()
- keep(), discard(), and List Filtering