Closures & Function Factories
Reviewed & published by Brayan K
A closure is a function that carries with it the environment in which it was created, letting it remember and even update variables from that enclosing scope long after the outer function has returned.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll write function factories that build specialized functions, understand why each closure has private state, and use the super-assignment operator to persist that state between calls.
What You'll Learn in This Lesson
- What a closure is and how it captures its environment
- How a function factory returns a customized function
- Why each closure gets its own private state
- How lexical scoping resolves captured variables
- When and why to use the <<- super-assignment operator
- How to build a stateful counter and accumulator
1️⃣ Functions That Return Functions
A function factory is a function whose return value is itself a function. The inner function captures the factory's arguments, so calling the factory with different values stamps out different specialized functions — here, a double and a triple from one make_multiplier.
# A function factory is a function that RETURNS a function.
# The returned function "remembers" the values from where it was created.
# This factory makes custom multiplier functions:
make_multiplier <- function(factor) {
function(x) { # the returned function captures 'factor'
x * factor
}
}
double <- make_multiplier(2) # factor is fixed at 2, forever
triple <- make_multiplier(3) # a separate function with factor 3
double(10) # -> [1] 20
triple(10) # -> [1] 30
double(7) # -> [1] 142️⃣ Closures and Private State
The returned function plus its captured environment is a closure. Each closure holds its own copy of the captured variables, so hi and howdy never interfere with each other even though they came from the same factory.
# A CLOSURE is the function + the environment it carries with it.
# Each closure has its OWN copy of the captured variables.
make_greeter <- function(greeting) {
function(name) {
paste0(greeting, ", ", name, "!")
}
}
hi <- make_greeter("Hi")
howdy <- make_greeter("Howdy")
hi("Ada") # -> [1] "Hi, Ada!"
howdy("Bob") # -> [1] "Howdy, Bob!"
# 'hi' and 'howdy' are independent — changing one never affects the other.3️⃣ Persisting State with <<-
To make a closure that changes its captured state between calls, use the super-assignment operator <<-. A plain <- would create a fresh local variable each call; <<- reaches up to update the captured one, giving the closure memory.
# To CHANGE a captured variable, use the super-assignment operator <<-.
# Plain <- would create a NEW local variable; <<- updates the captured one.
make_counter <- function() {
count <- 0 # lives in the enclosing environment
function() {
count <<- count + 1 # update the captured 'count', not a local copy
count
}
}
next_id <- make_counter()
next_id() # -> [1] 1
next_id() # -> [1] 2
next_id() # -> [1] 3
# A second counter starts fresh — it has its OWN count:
other <- make_counter()
other() # -> [1] 1Your turn. Fill in the ___ blanks, run it, and compare with the expected output.
# 🎯 YOUR TURN — replace each ___ then run it.
# Build a factory that makes "power" functions: square, cube, etc.
make_power <- function(exponent) {
function(x) {
x ^ ___ # 👉 replace ___ with exponent
}
}
square <- make_power(2)
cube <- ___ # 👉 replace ___ with make_power(3)
print(square(5)) # expect 25
print(cube(2)) # expect 8
print(square(9)) # expect 81
# ✅ Expected output:
# [1] 25
# [1] 8
# [1] 81Mini-Challenge: Running Total
Write an accumulator that keeps a running total in its closure with <<-. Then prove a second accumulator has independent state — the heart of closures.
# 🎯 MINI-CHALLENGE: a running-total accumulator.
#
# Write make_accumulator() that returns a function. Each call ADDS its
# argument to a running total kept inside the closure, and returns the
# new total. Use <<- to update the captured total.
#
# acc <- make_accumulator()
# acc(10) # 10
# acc(5) # 15
# acc(100) # 115
#
# Then make a SECOND accumulator and confirm it starts from 0 again,
# proving each closure has its own private state.
#
# ✅ Expected: 10, 15, 115, then 3 for a fresh acc2(3)
# your code hereCommon Errors (and the fix)
- Using <- when you meant <<- — ❌ the counter resets every call. ✅ Use count <<- count + 1 to update the captured variable.
- Forgetting to return the inner function — ❌ the factory returns the last value, not a function. ✅ Make the inner function(...) {...} the last expression.
- Expecting shared state across closures — ❌ each closure is independent. ✅ Build one closure and reuse it if you want shared state.
- Overusing <<- — ❌ super-assignment everywhere makes code unpredictable. ✅ Reserve it for deliberate captured-state updates.
Pro Tips
- 💡 Inspect the captured env with environment(double) and ls(environment(double)).
- 💡 Force arguments with force(factor) in a factory to avoid lazy-evaluation surprises in loops.
- 💡 Functionals love factories: pass factory-made functions straight into sapply() or purrr::map().
📋 Quick Reference — Closures
| Concept | R Syntax |
|---|---|
| Function factory | f <- function(a) function(x) x + a |
| Make a specialized fn | add5 <- f(5) |
| Update captured state | count <<- count + 1 |
| See captured vars | ls(environment(add5)) |
| Force a captured arg | force(a) |
🎉 Lesson Complete!
- ✅ A function factory returns a function customized by its arguments
- ✅ A closure is that function plus its captured environment
- ✅ Each closure has its own private state
- ✅ Lexical scoping resolves variables by where code was written
- ✅ <<- updates captured state so a closure can remember
Practice quiz
What is a function factory in R?
- A function with no arguments
- A built-in package for functions
- A function that returns another function
- A vector of functions
Answer: A function that returns another function. A function factory returns a new, specialized function as its value.
What is a closure?
- A function bundled with the environment it was created in
- A loop that never ends
- A way to close a file connection
- A type of vector
Answer: A function bundled with the environment it was created in. A closure is a function plus its captured enclosing environment.
In make_multiplier <- function(factor) function(x) x * factor, what does double <- make_multiplier(2) capture?
- x fixed at 2
- nothing; factor stays free
- the whole global environment only
- factor fixed at 2
Answer: factor fixed at 2. The returned function captures factor = 2 from where it was created.
Why use <<- instead of <- to update captured state in a counter?
- <<- is faster than <-
- <<- updates the variable in the enclosing environment
- <- cannot appear inside functions
- <<- creates a new local variable
Answer: <<- updates the variable in the enclosing environment. Plain <- makes a new local; <<- reaches up to update the captured variable.
Two counters made from the same make_counter() factory...
- Each have their own independent count
- Share a single global count
- Always error on the second call
- Must use the same variable name
Answer: Each have their own independent count. Each call to the factory creates a fresh environment with its own state.
What does lexical scoping mean for closures?
- Variables resolve based on where the function is called
- All variables are global
- Variables resolve based on where the function was defined
- Functions cannot see outer variables
Answer: Variables resolve based on where the function was defined. R uses lexical scoping: lookup follows the definition site, not the call site.
What happens if a factory forgets to return the inner function?
- R raises a syntax error
- You get the last evaluated value, not a function
- The inner function still runs automatically
- The factory returns NULL always
Answer: You get the last evaluated value, not a function. The factory must make the inner function its last expression to return it.
What does count <<- count + 1 require to already exist?
- A global named count2
- An integer literal named count
- The package methods loaded
- A count variable in an enclosing environment
Answer: A count variable in an enclosing environment. <<- searches enclosing environments for count to update it.
After double <- make_multiplier(2), what does double(7) return?
- 9
- 14
- 2
- 7
Answer: 14. double multiplies its argument by the captured factor 2, so 7 * 2 = 14.
Which call inspects the variables captured by a closure named add5?
- names(add5)
- str(add5())
- ls(environment(add5))
- environmentName(add5)
Answer: ls(environment(add5)). ls(environment(add5)) lists the names bound in the closure's environment.
Continue this course
- Previous: Reports with R Markdown
- Next: Debugging R (browser, traceback) — Find and fix bugs with traceback, browser, debug, and tryCatch
- Quick reference: R cheat sheet
Frequently asked questions
What exactly is a closure in R?
A closure is a function bundled together with the environment in which it was created. When you define a function inside another function, the inner function keeps a reference to its enclosing environment, so it can still see the outer function's variables even after that outer function has finished running. In R, almost every function you write is technically a closure — the term becomes interesting when the captured variables outlive the call that created them, as in a function factory.
Why do I need <<- instead of <- inside a closure?
The regular assignment <- always creates or updates a variable in the current (local) environment. So inside a counter, count <- count + 1 would make a brand-new local count, throwing away the captured one on every call. The super-assignment operator <<- instead walks up to the enclosing environment and updates the captured variable there, which is what lets state persist between calls. Use <<- deliberately and only for this purpose — overusing it makes code hard to follow.
What is lexical scoping and how does it relate to closures?
Lexical (or static) scoping means a function looks up variables based on where it was defined in the source code, not where it is called from. So when make_multiplier returns its inner function, that inner function resolves 'factor' by looking in the environment where it was written — the body of make_multiplier — capturing the value that was present there. Closures are lexical scoping made visible: the captured environment travels with the function.
Where would I actually use a function factory?
Function factories shine when you need many similar functions that differ only by a parameter: a family of currency formatters (USD, EUR), validators with different thresholds, or custom ggplot2 themes. They also power stateful tools like counters, rate limiters, memoized caches, and ID generators, where each instance keeps private state. The pattern keeps configuration in one place and hands back ready-to-use specialized functions, which is cleaner than repeating arguments at every call site.