6.4 Lexical scoping
In Chapter 2, we discussed assignment, the act of binding a name to a value. Here we’ll discuss scoping, the act of finding the value associated with a name.
The basic rules of scoping are quite intuitive, and you’ve probably already internalised them, even if you never explicitly studied them. For example, what will the following code return, 10 or 20?25
10
x <- function() {
g01 <- 20
x <-
x
}
g01()
In this section, you’ll learn the formal rules of scoping as well as some of its more subtle details. A deeper understanding of scoping will help you to use more advanced functional programming tools, and eventually, even to write tools that translate R code into other languages.
R uses lexical scoping26: it looks up the values of names based on how a function is defined, not how it is called. “Lexical” here is not the English adjective that means relating to words or a vocabulary. It’s a technical CS term that tells us that the scoping rules use a parse-time, rather than a run-time structure.
R’s lexical scoping follows four primary rules:
- Name masking
- Functions versus variables
- A fresh start
- Dynamic lookup
6.4.1 Name masking
The basic principle of lexical scoping is that names defined inside a function mask names defined outside a function. This is illustrated in the following example.
10
x <- 20
y <- function() {
g02 <- 1
x <- 2
y <-c(x, y)
}g02()
#> [1] 1 2
If a name isn’t defined inside a function, R looks one level up.
2
x <- function() {
g03 <- 1
y <-c(x, y)
}g03()
#> [1] 2 1
# And this doesn't change the previous value of y
y#> [1] 20
The same rules apply if a function is defined inside another function. First, R looks inside the current function. Then, it looks where that function was defined (and so on, all the way up to the global environment). Finally, it looks in other loaded packages.
Run the following code in your head, then confirm the result by running the code.27
1
x <- function() {
g04 <- 2
y <- function() {
i <- 3
z <-c(x, y, z)
}i()
}g04()
The same rules also apply to functions created by other functions, which I call manufactured functions, the topic of Chapter 10.
6.4.2 Functions versus variables
In R, functions are ordinary objects. This means the scoping rules described above also apply to functions:
function(x) x + 1
g07 <- function() {
g08 <- function(x) x + 100
g07 <-g07(10)
}g08()
#> [1] 110
However, when a function and a non-function share the same name (they must, of course, reside in different environments), applying these rules gets a little more complicated. When you use a name in a function call, R ignores non-function objects when looking for that value. For example, in the code below, g09
takes on two different values:
function(x) x + 100
g09 <- function() {
g10 <- 10
g09 <-g09(g09)
}g10()
#> [1] 110
For the record, using the same name for different things is confusing and best avoided!
6.4.3 A fresh start
What happens to values between invocations of a function? Consider the example below. What will happen the first time you run this function? What will happen the second time?28 (If you haven’t seen exists()
before, it returns TRUE
if there’s a variable with that name and returns FALSE
if not.)
function() {
g11 <-if (!exists("a")) {
1
a <-else {
} a + 1
a <-
}
a
}
g11()
g11()
You might be surprised that g11()
always returns the same value. This happens because every time a function is called a new environment is created to host its execution. This means that a function has no way to tell what happened the last time it was run; each invocation is completely independent. We’ll see some ways to get around this in Section 10.2.4.
6.4.4 Dynamic lookup
Lexical scoping determines where, but not when to look for values. R looks for values when the function is run, not when the function is created. Together, these two properties tell us that the output of a function can differ depending on the objects outside the function’s environment:
function() x + 1
g12 <- 15
x <-g12()
#> [1] 16
20
x <-g12()
#> [1] 21
This behaviour can be quite annoying. If you make a spelling mistake in your code, you won’t get an error message when you create the function. And depending on the variables defined in the global environment, you might not even get an error message when you run the function.
To detect this problem, use codetools::findGlobals()
. This function lists all the external dependencies (unbound symbols) within a function:
::findGlobals(g12)
codetools#> [1] "+" "x"
To solve this problem, you can manually change the function’s environment to the emptyenv()
, an environment which contains nothing:
environment(g12) <- emptyenv()
g12()
#> Error in x + 1: could not find function "+"
The problem and its solution reveal why this seemingly undesirable behaviour exists: R relies on lexical scoping to find everything, from the obvious, like mean()
, to the less obvious, like +
or even {
. This gives R’s scoping rules a rather beautiful simplicity.
6.4.5 Exercises
What does the following code return? Why? Describe how each of the three
c
’s is interpreted.10 c <-c(c = c)
What are the four principles that govern how R looks for values?
What does the following function return? Make a prediction before running the code yourself.
function(x) { f <- function(x) { f <- function() { f <-^ 2 x }f() + 1 }f(x) * 2 }f(10)
I’ll “hide” the answers to these challenges in the footnotes. Try solving them before looking at the answer; this will help you to better remember the correct answer. In this case,
g01()
will return20
.↩︎Functions that automatically quote one or more arguments can override the default scoping rules to implement other varieties of scoping. You’ll learn more about that in Chapter 20.↩︎
g04()
returnsc(1, 2, 3)
.↩︎g11()
returns1
every time it’s called.↩︎