Advanced R Exercises: 10 Functional Programming Practice Problems, Solved Step-by-Step
Sharpen your functional programming skills in R with 10 hands-on exercises covering pure functions, first-class functions, higher-order operations (map, filter, reduce), immutability, closures, and pipeline composition, each with starter code and a fully worked solution.
These exercises follow the same progression as the Functional Programming in R tutorial. Work through them in order, earlier problems build habits you need for the later ones. Type your answer before opening the solution; the struggle is where the learning happens.
How Should You Use These Exercises?
Every code block on this page shares a single R session, so variables you create in one exercise carry forward to the next. Let's confirm that with a quick warm-up.
That variable now exists for the rest of this page. Each exercise gives you a starter block with a skeleton and expected output, plus a collapsible worked solution with a line-by-line explanation. Aim to solve it yourself first.
Exercise 1: Can You Write a Pure Function That Scales a Vector?
A pure function takes its inputs and returns a result, no globals, no side effects, same input always gives the same output. Your job: write scale_between(x, low, high) that rescales a numeric vector x to fall within [low, high].
Click to reveal solution
Explanation: First we normalise x to the 0-1 range by subtracting the minimum and dividing by the range. Then we stretch it to [low, high] by multiplying by the target width and adding low. The function touches nothing outside its own body, pure by construction.
Exercise 2: Can You Spot and Fix the Impure Function?
The function below tracks a running total using <<-, which writes to the global environment. That makes it impure, calling it twice with the same input gives different results. Rewrite it so the same inputs always produce the same output.
Click to reveal solution
Explanation: The impure version hid state in a global variable, making the output depend on when you call it. The pure version takes the current total as an explicit argument, so the output depends only on the inputs. To accumulate, you chain calls or use Reduce, the state travels through the function, not around it.
<<- operator is a code smell in functional R. Every time you see <<-, it means a function is reaching outside its own scope to change something. Replace that hidden state with an explicit argument and the function becomes testable, predictable, and safe to run in parallel.Exercise 3: Can You Store Functions in a List and Dispatch by Name?
In R, functions are first-class values, you can store them in variables, lists, or pass them as arguments. Create a named list of four summary statistics and write a dispatcher function.
Click to reveal solution
Explanation: stat_funs is a named list where each element is a function. stat_funs[[stat_name]] retrieves the function by name, and the trailing (x) calls it. This pattern is called "dispatch by name", it replaces long if/else chains with a clean lookup.
Exercise 4: Can You Build a Function Factory for Power Functions?
A function factory is a function that returns a new function. The returned function "closes over" (remembers) the variables from its creation environment. Write make_power(n) that returns a function raising its argument to the nth power.
Click to reveal solution
Explanation: make_power(2) creates a new function whose body is x^n, where n is locked to 2 in the enclosing environment. That binding persists even after make_power finishes. This is a closure, the returned function "closes over" n. The 0.5 test shows that square roots are just power-0.5, and the factory handles that without any special case.
force(n) inside the factory to lock in the current value.Exercise 5: Can You Replace a For Loop With sapply?
Higher-order functions like sapply replace explicit loops with a single, declarative call. Below is a for loop that z-score normalises each column of a data frame. Rewrite it as a one-liner using sapply.
Click to reveal solution
Explanation: sapply(df, fun) applies the anonymous function to each column and simplifies the result to a matrix. Wrapping it in as.data.frame() gives back a data frame. One line replaces four. More importantly, the intent, "normalise each column", is visible at a glance, while the loop buries it in index bookkeeping.
Exercise 6: Can You Chain Filter and Reduce to Solve a Data Problem?
Filter keeps elements that satisfy a predicate. Reduce collapses a sequence into a single value using a binary function. Combine them: given a mixed list, keep only the positive numbers and compute their running product.
Click to reveal solution
Explanation: Filter applies the predicate to each element. Strings fail is.numeric(), negatives fail x > 0, and TRUE is technically numeric but not > 0 in the way we want (it equals 1, so the predicate passes, if you want to exclude it, add !is.logical(x)). Reduce then folds * across the surviving values: 7 * 2 = 14, 14 * 5 = 70, 70 * 4 = 280.
accumulate = FALSE (the default) and provide an init value: Reduce(\*\, x, init = 1). The init acts as the identity element and also saves you from the empty-input crash.Exercise 7: Can You Write Your Own Map From Scratch?
The best way to understand a higher-order function is to build one. Implement my_map(x, f) that applies f to every element of x and returns a list, without using lapply, sapply, Map, purrr::map, or any apply variant.
Click to reveal solution
Explanation: We pre-allocate a list with vector("list", length(x)) to avoid growing the list inside the loop (which is slow). seq_along(x) generates indices safely even if x is empty. Then we apply f to each element and store the result. This is essentially what lapply does internally in C, you've just written the R version.
sapply, Filter, and Reduce isn't that they avoid loops, it's that they give the loop a name. When you see sapply, you know "one call per element, collect results." When you see a raw for loop, you have to read the body to know what pattern it follows.Exercise 8: Can You Prove That Copy-on-Modify Keeps Your Data Safe?
R's copy-on-modify rule means a function cannot corrupt the data you pass in. Write a function mangle(df) that sorts the rows, renames a column, and adds a new column, then prove the original data frame is identical before and after the call.
Click to reveal solution
Explanation: Inside mangle, every modification triggers R's copy-on-modify: the df inside the function becomes a private copy the moment we sort, rename, or add a column. The caller's original_df is never touched. This is why functional R code is safe for data analysis, mistakes inside a function cannot retroactively poison your source data.
Exercise 9: Can You Compose Three Functions Into a Single Pipeline?
Function composition means chaining small, focused functions together. Below are three helpers that each do one text-cleaning step. Combine them into a single pipeline using |> that takes a messy character vector and returns a clean one.
Click to reveal solution
Explanation: The native pipe |> passes the left-hand result as the first argument of the next function. trimws() and tolower() both take a character vector as their first argument, so they work directly. gsub needs the input as its third argument (x), so we use the _ placeholder to tell the pipe where to put it. Each function does one thing; the pipe composes them into a readable left-to-right flow.
_ placeholder works only in named arguments. You can write gsub(pattern = "[[:punct:]]", replacement = "", x = _) but not gsub("[[:punct:]]", "", _). If you need positional placeholders, use magrittr's %>% with . instead.Exercise 10: Can You Build a Memoised Fibonacci Function?
Memoisation caches the results of expensive function calls so repeated calls with the same input return instantly. The naive recursive Fibonacci is painfully slow for large n because it recomputes the same values over and over. Build a memoised version using a closure.
Click to reveal solution
Explanation: make_fib_memo creates an environment (cache) that lives as long as the returned function does, this is a closure in action. On each call, fib_inner first checks if the result is already cached. If yes, it returns instantly. If no, it computes the value recursively, stores it in cache, and returns it. The naive version computes fib(30) with over a billion recursive calls; the memoised version computes each value exactly once, 30 calls total.
Summary
| Exercise | Concept | Key Takeaway |
|---|---|---|
| 1 | Pure functions | Same input, same output, no side effects |
| 2 | Pure vs impure | Replace <<- with explicit arguments |
| 3 | First-class functions | Store functions in lists for clean dispatch |
| 4 | Function factories | Closures capture their enclosing environment |
| 5 | Map (sapply) | Replace column-wise loops with one declarative call |
| 6 | Filter + Reduce | Chain higher-order functions for complex logic |
| 7 | Build your own map | Understanding HOFs = understanding the loop they hide |
| 8 | Immutability | Copy-on-modify guarantees your data stays safe |
| 9 | Composition | Pipes chain small functions into readable flows |
| 10 | Memoisation | Closures + caching = exponential → linear performance |
References
- Wickham, H., Advanced R, 2nd Edition. Chapter 6: Functions. Link
- Wickham, H., Advanced R, 2nd Edition. Chapter 9: Functionals. Link
- Wickham, H., Advanced R, 2nd Edition. Chapter 10: Function Factories. Link
- purrr package documentation, Functional programming tools for R. Link
- R Core Team, base R
Reduce,Filter,Map, andPositionreference. Link - R Core Team, An Introduction to R. Link
Continue Learning
- Functional Programming in R, the parent tutorial covering all five concepts these exercises test.
- Base R's Functional Triad: Reduce(), Filter(), Map(), deep dive into the three base R higher-order functions used in exercises 6 and 7.
- purrr map() Variants, typed map alternatives when you want vectors instead of lists.