R Programming • Functions and Program Structure

Scoping & Environments — Understand scoping and environments (lexical scoping) conceptually

How R resolves variable names through nested environments and lexical scoping rules inherited from Scheme.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 6 figures

SECTION 1

Historical Context & Motivation

The question of how a programming language determines which value a variable name refers to — the scoping problem — is one of the oldest and most consequential design decisions in language theory. Early languages like Fortran and LISP initially used strategies that seemed intuitive to implementers but led to subtle, hard-to-diagnose bugs when programs grew in complexity. The shift toward lexical scoping, where a variable's binding is determined by the textual structure of the source code rather than the runtime call stack, represented a pivotal moment in language design. R inherits this philosophy directly from Scheme, placing it in a lineage of languages that prioritize referential transparency and predictability of name resolution.

1958 • LISP and Dynamic Scoping

John McCarthy's LISP introduced dynamic scoping as an implementation artifact. Variable lookups walked the call stack at runtime, meaning a function's behavior depended on its caller — an unpredictable design that caused widespread confusion as programs scaled.

1964 • Algol 60 and Static Scope

The Algol 60 report formalized static (lexical) scoping, where a variable's binding is determined by its position in the source text. This allowed compilers and programmers alike to reason about name resolution without executing the code.

1975 • Scheme Embraces Lexical Scope

Sussman and Steele created Scheme, a LISP dialect that adopted lexical scoping and first-class closures. This demonstrated that a dynamically typed, functional language could still provide predictable name resolution.

1993 • R Inherits Scheme's Scoping

Ross Ihaka and Robert Gentleman designed R at the University of Auckland, deliberately adopting Scheme's lexical scoping model. This choice, unusual among statistical languages of the era, gave R powerful support for closures, function factories, and modular program structure.

2000s • Environments as First-Class Objects

As the R ecosystem matured, the language's environment objects became central to package namespaces, non-standard evaluation in tidyverse, and metaprogramming. Understanding environments shifted from an academic concern to a practical necessity for R developers.

The fundamental question that scoping answers is deceptively simple: when R encounters a symbol like x inside a function body, how does it decide which object x refers to? Is it the x defined inside the function, the one in the calling function, or the one at the top level? R's answer — look where the function was defined, not where it was called — has profound implications for how we design, compose, and debug R programs.

SECTION 2

Core Principles & Definitions

To understand scoping in R, we need to internalize four interconnected concepts: environments, bindings, the parent-environment chain, and the lexical scoping rule itself. These form the mental model that allows you to trace any variable lookup in R code by reading the source text alone, without ever running the program.

1

Environment

An environment is a data structure that maps names (symbols) to values (objects). Every environment has a pointer to exactly one parent environment, forming a linked chain. The only exception is the empty environment, R_EmptyEnv, which has no parent and terminates the chain.
2

Binding

A binding is a name-value pair stored in an environment. When you write x <- 42, you create a binding from the symbol x to the value 42 in the current environment. Bindings can be overwritten but not duplicated within the same environment.
3

Lexical Scoping Rule

Lexical scoping means a function's free variables (variables used but not defined within it) are resolved in the environment where the function was defined, not the environment where it is called. This is also called static scoping.
4

Enclosing Environment

Every function object in R stores a reference to its enclosing environment — the environment that was active when the function was created. You can inspect it with environment(f). This pointer is the mechanism that implements lexical scoping: when R searches for a free variable, it follows this pointer upward through the chain.
5

Name Masking

Name masking occurs when a binding in a closer (child) environment shadows a binding with the same name in a parent environment. The lookup stops at the first match, so the innermost definition wins. This is why local variables inside a function can reuse names without affecting outer scopes.
✦

KEY TAKEAWAY

Think of environments as nested transparent folders on a desk. When you look for a document (variable), you start with the innermost folder. If it's not there, you open the next folder out, and so on until you either find it or run out of folders. In R, the nesting order of these folders is determined by where functions are written in the source code, not by the order in which they happen to be called at runtime. This is precisely what makes lexical scoping predictable — you can trace the lookup path just by reading the code.

SECTION 3

Visual Explanation — Environment Chains

The following diagram illustrates how R's environment chain works for a simple nested function scenario. Consider the code: x <- 1; f <- function() { y <- 2; g <- function() { z <- 3; x + y + z }; g() }; f(). When g() executes and needs to resolve x, it follows the parent pointers upward through the chain of enclosing environments until it finds a binding for x in the global environment.

Global Environmentx ← 1f ← function() { ... }f()'s Execution Environmenty ← 2g ← function() { ... }g()'s Execution Environmentz ← 3parentparentLookup: x + y + zz → found here ✓y → found in f()'s env ✓x → found in global env ✓Lexical scoping: lookups follow the definition-site chain, not the call stackg was defined inside f → g's parent is f's env → f's parent is global
Environment chain for nested functions. Each box represents an environment with its bindings. The upward arrows show parent-environment pointers that R follows when resolving free variables. The variable z is found locally, y is found one level up, and x is resolved at the global level.

Notice that the chain is determined entirely by where each function was defined in the source code. The function g was defined inside f, so g's enclosing environment is f's execution environment. Even if g were somehow passed to and called from a completely different function h, the lookup for y would still go to f's environment — never to h's. This is the essential distinction between lexical and dynamic scoping.

SECTION 4

How R Resolves Names — The Lookup Algorithm

R's name resolution can be described as a simple recursive algorithm. When the interpreter encounters a symbol s during evaluation, it performs a lookup in the current environment e. If s is bound in e, the associated value is returned. Otherwise, R recurses into parent.env(e) and repeats the process. If the empty environment is reached without finding a binding, R signals a "object not found" error.

NAME LOOKUP RULE

lookup(s,e)=ifs∈bindings(e)thene[s]elselookup(s,parent(e))lookup(s, e) = if s ∈ bindings(e) then e[s] else lookup(s, parent(e))

Where s is a symbol, e is an environment, bindings(e) is the set of names bound in e, and parent(e) is e's enclosing environment. Base case: lookup(s, emptyenv()) raises an error.

Four Key Rules of R Scoping

Hadley Wickham's Advanced R identifies four rules that together fully characterize R's scoping behavior.

1

Name Masking

A name defined inside a function masks the same name in any parent environment. The innermost binding always wins.
2

Functions vs. Variables

When R looks up a name used in a function-call position (e.g., f()), it skips non-function objects during the lookup, ensuring you don't accidentally call a number.
3

A Fresh Start

Each invocation of a function creates a new execution environment. Local bindings from a previous call do not persist — the function begins with a clean slate every time.
4

Dynamic Lookup

While the scope (where to look) is determined lexically at define-time, the actual value found at that location is determined at runtime. If a parent binding changes between calls, the function sees the new value.
⚠

Lexical vs. Dynamic — A Subtle Distinction

Under dynamic scoping (used by early LISP and some special constructs in R such as parent.frame()), free variables are resolved by walking the call stack — the sequence of functions that led to the current invocation. This means a function's behavior can change depending on who called it, which makes reasoning about correctness much harder. R uses lexical scoping by default but exposes dynamic scoping primitives for advanced metaprogramming.

SECTION 5

R's Environment Hierarchy

R maintains several distinct types of environments, each serving a specific role in the overall name-resolution architecture. Understanding these types clarifies how packages, namespaces, and user code interact. The diagram below shows the full environment search path that R traverses when resolving a name from within a function.

R Environment Search PathR_EmptyEnvbase environment (package:base)package:stats → package:utils → ... → base(search path / attached packages)R_GlobalEnv (user workspace)f()'s execution envg()'s execution envTerminates chainYour R consoleLookup directionStart at innermost env,walk UP toward R_EmptyEnvinspect with: search() and environmentName()
R's full environment search path. Lookups start at the innermost execution environment and walk upward through parent pointers. The global environment sits between user-defined function environments and the search path of attached packages, which terminates at the base environment and finally the empty environment.
Environment TypeCreated WhenAccess In R
Execution EnvironmentA function is called; destroyed when the call returns (unless captured by a closure)environment()
Enclosing EnvironmentA function is defined; stored as an attribute of the function objectenvironment(f)
Global EnvironmentR session starts; this is the interactive workspaceglobalenv() or .GlobalEnv
Package NamespaceA package is loaded; controls which names are exported vs. internalgetNamespace("pkg")
Empty EnvironmentAlways exists; the ultimate ancestor with no bindings and no parentemptyenv()

Key environment types in R and how to inspect them

SECTION 6

Worked Example — Closures and Counter Functions

One of the most powerful consequences of lexical scoping is the ability to create closures — functions that "remember" the environment in which they were created. Consider a classic counter factory that produces independent counter functions, each maintaining its own private state.

Building a Counter with Lexical Scoping

  1. 1

    Step 1 — Define the Factory Function

    We write a function make_counter that initializes a count variable and returns an inner function. The code is: make_counter <- function(start = 0) { count <- start; function() { count <<- count + 1; count } }. The <<- operator performs assignment in the parent (enclosing) environment rather than the current execution environment.
  2. 2

    Step 2 — Create Two Independent Counters

    When we call counter_a <- make_counter(0) and counter_b <- make_counter(100), R creates two separate execution environments for make_counter. Each environment contains its own count binding (initialized to 0 and 100, respectively). The returned inner functions are closures that retain pointers to these environments.
    counter_a's enclosing env has count = 0; counter_b's enclosing env has count = 100
  3. 3

    Step 3 — Call counter_a() Three Times

    Each call to counter_a() creates a fresh execution environment for the inner function. Inside, the symbol count is a free variable — it is not defined locally. By lexical scoping, R looks in the enclosing environment (the one created by make_counter(0)), finds count, increments it via <<-, and returns the new value.
    counter_a() returns 1, then 2, then 3
  4. 4

    Step 4 — Verify Independence

    Now call counter_b(). It returns 101, not 4. This confirms that counter_a and counter_b close over different environments. Their count variables are completely independent.
    counter_b() returns 101 — independent state confirmed
  5. 5

    Step 5 — Inspect with environment()

    We can verify the mechanism using environment(counter_a)$count which returns 3 (after three calls), and environment(counter_b)$count which returns 101 (after one call). The environment() function gives us direct access to the closure's enclosing environment, confirming that lexical scoping enables persistent, private state.
    Closures maintain encapsulated mutable state via their enclosing environments
🔒

CLOSURE INTUITION

A closure is like a function that carries a small backpack containing the variables it needs from its birthplace. No matter where the function travels (is passed as an argument, stored in a list, or returned from another function), it always has that backpack with it. The backpack's contents are determined by where the function was born (defined), not by where it currently happens to be running.

SECTION 7

Lexical vs. Dynamic Scoping — Comparison

Although R defaults to lexical scoping, dynamic scoping concepts surface frequently in R programming through functions like parent.frame(), sys.call(), and non-standard evaluation mechanisms in the tidyverse. Understanding the trade-offs between the two paradigms is essential for writing robust, maintainable code and for comprehending when R deliberately breaks its lexical scoping default.

PropertyLexical ScopingDynamic Scoping
Free variable resolutionFollows the chain of enclosing environments (definition site)Follows the call stack (calling site)
DeterminismA function's behavior depends only on its source context — predictable from reading the codeA function's behavior may change depending on who calls it — context-dependent
ClosuresNaturally supported; functions capture their enclosing environmentsNot meaningfully supported; no stable environment to close over
DebuggingEasier — variable origins are traceable by reading the sourceHarder — must trace the runtime call chain to find bindings
Use in RDefault behavior for all function definitionsAvailable via parent.frame(), formulas, and some base R functions like eval()
Languages using itR, Scheme, Python, JavaScript, Haskell, CEarly LISP, Emacs Lisp, bash, TeX

Comparison of lexical and dynamic scoping strategies

⚖️

DESIGN PHILOSOPHY

Lexical scoping favors local reasoning: you can understand a function by looking at its definition and its enclosing scope, without knowing all possible call sites. Dynamic scoping favors implicit parameterization: callers can inject context without the function declaring explicit parameters. R's decision to default to lexical scoping while offering dynamic scoping escape hatches reflects a pragmatic balance — safety by default, power when needed.

SECTION 8

Connection to Advanced R Concepts

The environment and scoping model is not merely an implementation detail — it is the conceptual foundation upon which several advanced R features are built. Package namespaces, non-standard evaluation (NSE), quosures in the rlang/tidyverse ecosystem, and R6 object-oriented programming all rely on explicit manipulation of environments and scoping rules. Gaining fluency with these foundational concepts prepares you to engage with these more sophisticated abstractions.

Concept Learned HereAdvanced ExtensionKey Mechanism
Enclosing environmentsClosures & function factoriesReturned functions capture the environment of the factory call, enabling parameterized behavior
Environment as data structureR6 classesR6 uses environments as mutable, reference-semantic containers for object state and methods
Parent chain & search pathPackage namespacesEach package has a namespace environment whose parent is its imports, enabling controlled name visibility
Lexical scoping of free variablesQuosures (rlang)A quosure bundles an expression with its environment, ensuring tidy evaluation respects the user's scoping context
Dynamic lookup via call stackNon-standard evaluationFunctions like subset() and dplyr::filter() evaluate expressions in data-frame environments, deliberately overriding normal scoping

How foundational scoping concepts connect to advanced R features

💡

Looking Ahead

When you study tidyverse programming or R package development, you will manipulate environments directly using rlang::env(), new.env(), and related functions. The mental model you build here — environments as linked nodes in a chain, with scoping determined by definition site — is the exact model you will apply when constructing custom evaluation contexts, writing package namespace declarations, or debugging unexpected variable bindings in production code.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain the difference between a function's execution environment and its enclosing environment. Why is this distinction critical for understanding closures?
Show answer
The execution environment is created fresh each time a function is called; it contains the function's local variables and formal parameters, and is typically discarded when the call returns. The enclosing environment is the environment where the function was originally defined, stored as a permanent attribute of the function object and accessed via environment(f). This distinction matters for closures because a closure's enclosing environment persists after the factory function that created it returns, allowing the closure to access (and modify) variables from the factory's scope. Without the enclosing environment pointer, closures would lose access to their captured state as soon as the creating call finished.

Question 2: BASIC CALCULATION

Consider the following R code: x <- 10; f <- function() { x <- 20; g <- function() { x }; g() }; f() What value does f() return? Trace the environment chain to justify your answer.
Show answer
The call f() returns 20. Here is the trace: when g() is called inside f, R evaluates the symbol x. It first looks in g's execution environment, which has no binding for x. Next, by lexical scoping, it looks in g's enclosing environment, which is f's execution environment where x <- 20. It finds x = 20 and returns it. The global x <- 10 is never reached because name masking stops the lookup at the first match.

Question 3: INTERMEDIATE

Now consider a slight modification: x <- 10; f <- function() { x <- 20; x }; g <- function() { x <- 30; f() }; g() What does g() return? Would the answer change under dynamic scoping? Explain both cases.
Show answer
Under R's lexical scoping, g() returns 20. When f() is called from within g, f defines its own local x <- 20 and returns it; g's x <- 30 is irrelevant because f's enclosing environment is the global environment (where it was defined), not g's execution environment. Under dynamic scoping, the answer would also be 20 in this particular case because f defines x locally. However, if f did NOT define x locally, dynamic scoping would find x = 30 in g's environment (the caller), whereas lexical scoping would find x = 10 in the global environment (the definition site).

Question 4: APPLIED

Write a function factory make_power(exp) that returns a closure raising its argument to the power exp. For example, square <- make_power(2); square(5) should return 25. Explain which variable is free in the closure and where it is resolved.
Show answer
The implementation is: make_power <- function(exp) { function(x) { x ^ exp } }. When make_power(2) is called, R creates an execution environment binding exp = 2. The returned anonymous function has x as a formal parameter (bound locally) and exp as a free variable. By lexical scoping, exp is resolved in the enclosing environment — the execution environment of the make_power(2) call — where it is bound to 2. You can verify this with environment(square)$exp, which returns 2. Creating cube <- make_power(3) produces an independent closure whose enclosing environment binds exp = 3.

Question 5: CRITICAL THINKING

R's "dynamic lookup" rule means that free variables are resolved at call time, not at definition time. Consider: y <- 1; h <- function() y; y <- 2; h() This returns 2, not 1. Discuss: is R truly a lexically scoped language if the value of a free variable can change after the function is defined? Construct an argument that reconciles dynamic lookup with the claim of lexical scoping.
Show answer
R is indeed lexically scoped because scoping determines where to look for a name, not when to snapshot its value. In the example, h's enclosing environment is the global environment (lexically determined at definition time). When h() is called, R looks up y in the global environment and finds the current binding, which is 2. The scoping rule (look in the global env) was fixed lexically; only the value in that environment changed. This contrasts with dynamic scoping, where a different environment might be consulted depending on the caller. The lesson is that lexical scoping guarantees a fixed lookup path but not a frozen snapshot. Languages that want frozen snapshots at definition time must implement eager binding or explicit capture, which R does not do by default due to its lazy evaluation model.

SUMMARY

Summary

Summary

R employs lexical scoping, inherited from Scheme, meaning that free variables in a function are resolved by looking in the enclosing environment — the environment where the function was defined — rather than the calling environment. An environment is a set of name-value bindings plus a pointer to a parent environment, forming a chain that terminates at the empty environment. Name resolution walks this chain upward until a match is found (name masking ensures the innermost binding wins).

The four key rules — name masking, functions vs. variables, a fresh start (new execution environment per call), and dynamic lookup (values resolved at call time, not definition time) — together define R's scoping behavior. Closures emerge naturally from this model: a returned function retains a reference to its enclosing environment, enabling persistent private state and powerful patterns like function factories. These concepts form the foundation for advanced R topics including package namespaces, non-standard evaluation, and quosures in rlang.

Varsity Tutors • R Programming • Scoping & Environments — Understand scoping and environments (lexical scoping) conceptually