Advanced R Exercises: 10 Functional Programming Practice Problems
Test your functional programming skills with 10 exercises covering closures, map()/Reduce(), function factories, composition, and error handling. Each problem has an interactive solution.
These exercises assume you're familiar with first-class functions, purrr map variants, closures, and function factories. They're designed to cement those concepts through hands-on practice.
Easy (1-3): Core FP Concepts
Exercise 1: First-Class Functions
Prove that functions are objects in R by storing them in a list and applying them dynamically.
# Create a list of 5 math functions: square, cube, negate, double, half
# Apply each one to the number 6 using sapply
**Explanation:** Functions are regular R objects — you can put them in lists, pass them to `sapply()`, and call them dynamically. This is the foundation of functional programming.
Exercise 2: Closures
Create a function that returns a counter — each call increments and returns the count.
# Create make_counter(start = 0)
# It should return a function that:
# - Returns the current count each time it's called
# - Increments the count after each call
# counter <- make_counter()
# counter() -> 1
# counter() -> 2
# counter() -> 3
**Explanation:** The inner function closes over `count` from the outer function's environment. `<<-` modifies `count` in the enclosing scope. Each call to `make_counter()` creates a separate environment.
Exercise 3: Map and Reduce
Given a list of numeric vectors, calculate the grand mean (mean of means) using map_dbl and Reduce.
library(purrr)
data_list <- list(
group_a = c(23, 45, 12, 67),
group_b = c(89, 34, 56),
group_c = c(11, 22, 33, 44, 55),
group_d = c(100, 200)
)
# 1. Calculate the mean of each group using map_dbl
# 2. Find the overall mean of those group means
# 3. Use Reduce to concatenate all vectors, then compute grand mean
Click to reveal solution
```r
library(purrr)
data_list <- list(
group_a = c(23, 45, 12, 67),
group_b = c(89, 34, 56),
group_c = c(11, 22, 33, 44, 55),
group_d = c(100, 200)
)
# 1. Mean per group
group_means <- map_dbl(data_list, mean)
cat("Group means:", round(group_means, 1), "\n")
# 2. Mean of means (unweighted)
cat("Mean of means:", round(mean(group_means), 1), "\n")
# 3. Grand mean (weighted by group size)
all_values <- Reduce(c, data_list)
cat("Grand mean:", round(mean(all_values), 1), "\n")
cat("(These differ because groups have different sizes)\n")
**Explanation:** `map_dbl` extracts one number per group. `Reduce(c, list)` concatenates all vectors into one. The mean-of-means and grand mean differ when groups have unequal sizes.
Medium (4-7): Combining Concepts
Exercise 4: Function Factory
Build a make_scaler() factory that creates scaling functions for different ranges.
# Create make_scaler(new_min, new_max) that returns a function
# The returned function scales input to [new_min, new_max] range
# scale_0_1 <- make_scaler(0, 1)
# scale_0_1(c(10, 20, 30, 40, 50)) -> c(0, 0.25, 0.5, 0.75, 1)
# scale_neg1_1 <- make_scaler(-1, 1)
# scale_neg1_1(c(10, 20, 30, 40, 50)) -> c(-1, -0.5, 0, 0.5, 1)
**Explanation:** The factory captures `new_min` and `new_max` in the closure. The returned function computes min-max scaling using both the captured range and the data's actual range.
Exercise 5: Compose a Data Pipeline
Use Reduce or purrr compose to build a text processing pipeline.
library(purrr)
# Create individual functions:
# 1. remove_digits: removes all digits
# 2. remove_extra_spaces: collapses multiple spaces to one
# 3. trim_and_lower: trims whitespace and lowercases
# Compose them into a single clean_text function
# Test on: " Hello 123 World 456 "
# Expected: "hello world"
**Explanation:** `compose()` chains functions. With `.dir = "forward"`, they run left-to-right. The `Reduce` approach applies each function in sequence, passing the result forward.
Exercise 6: safely() Error Handling
Process a mixed list of inputs, capturing both successes and failures.
library(purrr)
inputs <- list(4, "abc", 9, NULL, 16, "xyz", 25)
# Use safely(sqrt) to attempt sqrt on each input
# Collect all successful results into one vector
# Collect all error messages into another vector
**Explanation:** `safely()` wraps each result in a list with `$result` and `$error`. Use `keep()` and `discard()` to separate successes from failures. This is the FP way to handle errors — no try/catch spaghetti.
Exercise 7: imap with Reporting
Use imap to create a formatted report from a named list of statistics.
library(purrr)
quarterly_sales <- list(
Q1 = c(150, 200, 180, 220),
Q2 = c(250, 300, 275, 310),
Q3 = c(180, 190, 210, 200),
Q4 = c(350, 400, 380, 420)
)
# Use imap to create a report line for each quarter:
# "Q1: total=$750, avg=$187.5, best=$220"
# Calculate which quarter had the highest total
**Explanation:** `imap_chr` passes both the value (`.x`) and the name (`.y`) to your function. This is perfect for creating formatted output from named data.
Hard (8-10): Advanced Patterns
Exercise 8: Memoized Recursive Function
Write a memoized version of the Collatz sequence length calculator.
library(memoise)
# The Collatz conjecture: start with n
# If even: n/2. If odd: 3n+1. Repeat until n=1.
# Count the steps.
# Example: 6 -> 3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1 (8 steps)
# Write collatz_length(n) and memoize it
# Then find which number from 1 to 100 has the longest sequence
Click to reveal solution
```r
library(memoise)
collatz_length <- memoise(function(n) {
if (n == 1) return(0)
if (n %% 2 == 0) 1 + collatz_length(n / 2)
else 1 + collatz_length(3 * n + 1)
})
# Test
cat("Collatz length of 6:", collatz_length(6), "\n")
cat("Collatz length of 27:", collatz_length(27), "\n")
# Find the longest chain from 1 to 100
lengths <- sapply(1:100, collatz_length)
best <- which.max(lengths)
cat("\nLongest chain: n =", best, "with", lengths[best], "steps\n")
**Explanation:** Memoization caches intermediate results. When computing the chain for 6, it caches lengths for 3, 10, 5, 16, 8, 4, 2, 1. Later numbers reuse these cached values, making the sweep from 1-100 very fast.
Exercise 9: Function Operator Chain
Create a with_retry(f, n) function operator that retries a function up to n times on failure.
# Create with_retry(f, max_attempts = 3)
# It should return a new function that:
# - Calls f(...)
# - If it errors, retries up to max_attempts times
# - Returns the first successful result
# - If all attempts fail, throws the last error
# Test with a function that fails randomly
Click to reveal solution
```r
with_retry <- function(f, max_attempts = 3) {
function(...) {
for (i in seq_len(max_attempts)) {
result <- tryCatch(f(...), error = function(e) e)
if (!inherits(result, "error")) return(result)
if (i < max_attempts) cat(" Attempt", i, "failed, retrying...\n")
}
stop("All ", max_attempts, " attempts failed. Last error: ", result$message)
}
}
# Unreliable function: fails 60% of the time
set.seed(42)
unreliable <- function(x) {
if (runif(1) < 0.6) stop("Random failure!")
x * 10
}
safe_fn <- with_retry(unreliable, max_attempts = 5)
result <- safe_fn(7)
cat("Result:", result, "\n")
**Explanation:** `with_retry` is a function operator — it takes a function and returns an enhanced version. The retry loop uses `tryCatch` to catch errors and loop until success or exhaustion.
Exercise 10: Full FP Pipeline
Combine map, reduce, compose, and closures to build a complete data analysis pipeline.
library(purrr)
# Given: a list of CSV-like text strings (simulating file reads)
raw_data <- list(
"Alice,88,A", "Bob,76,C", "Carol,92,A",
"David,NA,B", "Eve,95,A", "Frank,81,B"
)
# 1. Parse each string into a named list (name, score, grade)
# 2. Filter out entries with NA scores
# 3. Calculate mean score of remaining entries
# 4. Find the person with highest score
Click to reveal solution
```r
library(purrr)
raw_data <- list(
"Alice,88,A", "Bob,76,C", "Carol,92,A",
"David,NA,B", "Eve,95,A", "Frank,81,B"
)
# 1. Parse: map each string to a named list
parse_record <- \(s) {
parts <- strsplit(s, ",")[[1]]
list(name = parts[1],
score = suppressWarnings(as.numeric(parts[2])),
grade = parts[3])
}
records <- map(raw_data, parse_record)
cat("Parsed", length(records), "records\n")
# 2. Filter: keep only non-NA scores
valid <- keep(records, \(r) !is.na(r$score))
cat("Valid:", length(valid), "records\n")
# 3. Mean score
scores <- map_dbl(valid, "score")
cat("Mean score:", mean(scores), "\n")
# 4. Highest score
best_idx <- which.max(scores)
cat("Top scorer:", valid[[best_idx]]$name,
"with", valid[[best_idx]]$score, "\n")
**Explanation:** This exercise chains `map` (parse), `keep` (filter), `map_dbl` (extract), and base R `which.max` (find). Each step is a pure function that transforms data — classic FP style.