Lexical Scoping Rules
Understand how R finds variables by searching parent environments.
Lexical Scoping Rules is a free R Academy lesson on CoddyKit — lesson 2 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.
What is Lexical Scoping?
Lexical scoping means a function looks up variables where it was defined, not where it is called. R uses lexical scoping. This is predictable: the environment a function searches is fixed at creation time, not at call time.
x <- 'global'
f <- function() {
cat('x =', x, '\n') # looks where f was defined (global)
}
local({
x <- 'local to local block'
f() # still prints global x, not the local one
})Lexical vs Dynamic Scoping
In dynamic scoping (used in some other languages), a function would find the variable in the calling environment. R uses lexical scoping instead: the enclosing environment at definition time always wins. This makes functions behave consistently regardless of where they are called from.
val <- 100
make_adder <- function(n) {
# n is captured from the definition environment
function(x) x + n
}
add10 <- make_adder(10)
add20 <- make_adder(20)
cat(add10(5), '\n') # 15 — n=10 from definition
cat(add20(5), '\n') # 25 — n=20 from definitionFree Variables and Closures
A variable inside a function that is not a parameter or local assignment is a free variable. R finds it in the enclosing environment. A function that captures free variables is called a closure.
base_rate <- 0.05 # free variable captured by f
f <- function(principal, years) {
principal * (1 + base_rate) ^ years
}
cat('5 yr return:', f(1000, 5), '\n')
# Even if we change base_rate later:
base_rate <- 0.08
cat('New rate return:', f(1000, 5), '\n')Closures Capture Environments
Each time a factory function is called, the returned function (closure) captures a snapshot of the enclosing environment. Different calls produce closures with different captured values — this is the basis of function factories.
power_fn <- function(exp) {
function(x) x ^ exp
}
square <- power_fn(2)
cube <- power_fn(3)
cat('square(4) =', square(4), '\n')
cat('cube(3) =', cube(3), '\n')The <<- Operator
<<- (double-arrow assignment) searches up the parent chain for an existing binding and modifies it in place. If no binding is found, it creates one in the global environment. It is the tool for modifying a variable in an enclosing scope from inside a function.
count <- 0
increment <- function() {
count <<- count + 1
}
increment()
increment()
increment()
cat('count:', count, '\n')<<- with Closures
The most idiomatic use of <<- is inside closures that need to update state in their enclosing environment. This creates stateful functions — functions with persistent memory between calls.
make_counter <- function() {
n <- 0
list(
increment = function() n <<- n + 1,
get = function() n,
reset = function() n <<- 0
)
}
ctr <- make_counter()
ctr$increment()
ctr$increment()
cat('Count:', ctr$get(), '\n')
ctr$reset()
cat('After reset:', ctr$get(), '\n')Scoping in Nested Functions
When functions are nested, the inner function can see variables defined in the outer function. The outer function's environment becomes the inner function's enclosing environment, forming a chain.
outer <- function() {
greeting <- 'Hello'
inner <- function(name) {
# greeting comes from outer's environment
paste(greeting, name)
}
inner('World')
}
cat(outer(), '\n')Looking Up the Scope Chain
When R looks up a variable, it searches environments in order: function's own → enclosing → enclosing's enclosing → ... → global → search path → base → empty. The first match wins.
x <- 'global'
f <- function() {
x <- 'local to f'
g <- function() {
# g has no x, so looks in f's env
cat('g sees x =', x, '\n')
}
g()
}
f()environment() of a Function
You can inspect the enclosing environment of any function with environment(f). You can even set it with environment(f) <- e, which changes where the function will look up free variables — a powerful but advanced technique.
x <- 'global_x'
f <- function() x
cat('f sees:', f(), '\n')
# Change f's enclosing environment
my_env <- new.env(parent = emptyenv())
my_env$x <- 'env_x'
environment(f) <- my_env
cat('f now sees:', f(), '\n')Practical Closure: Memoization
Lexical scoping + <<- enables memoization: cache expensive function results in the closure's environment. The cache is private to the closure and persists between calls.
make_memoized_fib <- function() {
cache <- c()
function(n) {
if (!is.null(cache[n])) return(cache[n])
if (n <= 1) { cache[n] <<- n; return(n) }
result <- Recall(n-1) + Recall(n-2)
cache[n] <<- result
result
}
}
fib <- make_memoized_fib()
cat(sapply(0:8, fib), '\n')Lexical Scoping Gotcha: Loop Variables
A classic gotcha: creating closures inside a loop. All closures capture the same environment, not a snapshot of the value. By the time they are called, the loop variable holds its final value.
fns <- vector('list', 3)
for (i in 1:3) {
local({
captured_i <- i
fns[[captured_i]] <<- function() captured_i
})
}
cat(fns[[1]](), fns[[2]](), fns[[3]](), '\n')Quick Check
In R's lexical scoping, where does a function look for a free variable (one not defined in the function itself)?
Lexical Scoping: Key Takeaways
Key takeaways for lexical scoping:
- R uses lexical scoping — functions look up variables where they were defined
- A function captures its enclosing environment as a closure
- Free variables are resolved at definition time, not call time
<<-modifies a binding in the parent chain (or global if not found)- Closures +
<<-= stateful functions with persistent private state - Loop closure gotcha: use
local()to force a new environment per iteration
make_multiplier <- function(factor) {
function(x) x * factor
}
double <- make_multiplier(2)
triple <- make_multiplier(3)
cat('double(7):', double(7), '\n')
cat('triple(7):', triple(7), '\n')
cat('double env factor:', get('factor', envir = environment(double)), '\n')Frequently asked questions
Is the “Lexical Scoping Rules” lesson free?
Yes — the full text of “Lexical Scoping Rules” 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 “Lexical Scoping Rules”?
Understand how R finds variables by searching parent environments. 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 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Lexical Scoping Rules” 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.