Functional Programming in R
Introduction
Functional programming in R isn’t a niche trick—it’s how the language was designed to work. R treats functions as first-class citizens, which means you can pass them around, return them, and store them in data structures just like any other value. This opens up a different way of thinking about problems: decompose your task into transformations, then apply them systematically.
First-Class Functions
In R, functions are objects you can assign to variables, put in lists, and pass as arguments to other functions. This is the foundation everything else builds on. Unlike languages that separate “functions” from “data,” R blurs that line deliberately.
# Assign a function to a variable
square <- function(x) x^2
# Pass a function to another function
result <- sapply(1:5, square)
# [1] 1 4 9 16 25
You can also store multiple functions in a list for dynamic selection:
operations <- list(
double = function(x) x * 2,
triple = function(x) x * 3,
square = function(x) x^2
)
choose_op <- function(name, x) {
operations[[name]](x)
}
choose_op("double", 5)
# [1] 10
This pattern shows up constantly in R programming. You’re not just calling functions—you’re composing behavior. Anonymous functions (those without names) work the same way and are common in data transformation pipelines.
Higher-Order Functions
A higher-order function is simply a function that takes other functions as input or returns them as output. R’s apply family is the most common example, but you can build your own. Understanding this concept lets you abstract away repetition and write more reusable code.
# A function that takes a function and applies it twice
apply_twice <- function(f, x) {
f(f(x))
}
apply_twice(function(x) x + 1, 0)
# [1] 2
The lapply and sapply functions iterate over lists and vectors, applying a function to each element. This replaces explicit loops for many tasks:
data <- list(c(1,2,3), c(4,5,6), c(7,8,9))
means <- lapply(data, mean)
# [[1]]
# [1] 2
# [[2]]
# [1] 5
# [[3]]
# [1] 8
The key advantage is that you don’t manage iteration state yourself—the function handles it. This reduces bugs and makes your code easier to reason about.
Closures
A closure is a function that captures variables from its creation environment. This sounds abstract but is incredibly useful for creating functions with persistent state. The captured variables live as long as the function itself, allowing you to maintain state without resorting to global variables.
# Create a counter function
make_counter <- function() {
count <- 0
function() {
count <<- count + 1
count
}
}
counter1 <- make_counter()
counter2 <- make_counter()
counter1() # [1] 1
counter1() # [1] 2
counter2() # [1] 1
counter2() # [1] 2
Each call to make_counter() creates a fresh environment with its own count variable. The inner function “closes over” that environment, preserving it between calls. This is the same mechanism used in R’s formula notation and in packages like dplyr for creating scoped variants of functions. When you call filter(df, across(everything(), ~ .x > 0)), the .x is bound through closure semantics.
Function Factories
A function factory is a function that returns other functions. Use them when you need many similar functions that differ only in some parameter.
# Create a power function with a fixed exponent
power <- function(exp) {
function(base) {
base^exp
}
}
square <- power(2)
cube <- power(3)
square(5) # [1] 25
cube(5) # [1] 125
Factories are useful for creating families of related functions:
# Create scalers for different transformations
scale_by <- function(factor) {
function(x) x * factor
}
scale_10 <- scale_by(10)
scale_100 <- scale_by(100)
scale_10(c(1, 2, 3)) # [1] 10 20 30
Practical Examples
Combining these concepts lets you write concise, expressive code:
# Example 1: Using closures with lapply
make_multipliers <- function(vals) {
lapply(vals, function(x) function(y) x * y)
}
mults <- make_multipliers(c(2, 3, 4))
mults[[2]](10) # [1] 30 (10 * 3)
# Example 2: Custom higher-order function
filter_by <- function(data, condition_fn) {
data[sapply(data, condition_fn)]
}
numbers <- list(1, 5, 10, 15, 20, 25)
is_even <- function(x) x %% 2 == 0
filter_by(numbers, is_even)
# [[1]]
# [1] 10
# [[2]]
# [1] 20
These patterns appear throughout R’s ecosystem. The tidyverse builds heavily on them—purrr’s functional programming tools, dplyr’s scoped functions, and ggplot2’s layer specifications all use closures and function factories under the hood.
See Also
- Functional Programming with purrr — functional tools for working with vectors and lists
- The Apply Family — lapply, sapply, vapply, and mapply explained
- Lists vs Vectors in R — understanding R’s fundamental data structures