Programming for Applications

Chapter 8: Symbols and Environments

Yu-You Liou (NTU)

Shih Chien University

2026-08-03

Where Names Live

Environments, Finally Defined

We have danced around environments since Chapter 3; time to define them. An environment is an R object containing:

  • the set of symbols available in a given context,
  • the objects associated with those symbols (together: the frame), and
  • a pointer to a parent environment.

Every evaluation context has an environment. To resolve a symbol, R searches the current environment first, then recursively searches parent environments until it finds a match.

Symbols

Resolution Happens at Construction Time

Defining a variable assigns a symbol to a value in an environment. When an object is built from symbols, R resolves them at construction time:

x <- 1; y <- 2; z <- 3
v <- c(x, y, z)
v
[1] 1 2 3
x <- 10      # v was already built; changing x changes nothing
v
[1] 1 2 3

Delaying Evaluation: quote and eval

You can postpone symbol resolution by quoting the expression and evaluating it later — each eval resolves symbols as of now:

x <- 1; y <- 2; z <- 3
v <- quote(c(x, y, z))
eval(v)
[1] 1 2 3
x <- 5
eval(v)
[1] 5 2 3

Promises: delayedAssign

A promise delays evaluation until the variable is first used:

x <- 1; y <- 2; z <- 3
delayedAssign("v", c(x, y, z))
x <- 5
v          # evaluated now — with the current x
[1] 5 2 3

Packages use promises to expose objects without loading them into memory. Two caveats: you cannot test whether an object is a promise, nor recover the environment where one was created.

Working with Environments

The Environment Toolbox

Environments are objects (internally, hash tables of symbol mappings — an efficiency trick exploited in Chapter 24). The vocabulary:

Function Description
assign / get / exists bind, fetch, or test a name in an environment
objects all names defined in an environment
remove (rm) drop objects from an environment
search names of attached packages — the chain of parent environments
searchpaths paths of attached packages
attach / detach add/remove a list, data frame, or data file to/from the search path
emptyenv the empty environment — every chain ends here
parent.env the parent of an environment
baseenv the environment of the base package
globalenv / .GlobalEnv the user’s workspace
environment a function’s environment (no argument: the current one)
new.env a fresh environment object

objects and rm

The two everyday calls — what’s here, and clean it up:

rm(list = ls())      # start with a clean slate
x <- 1; y <- 2; z <- 3
objects()
[1] "x" "y" "z"
rm(x)
objects()
[1] "y" "z"

The Global Environment

Each session gets a fresh environment for your objects: the global environment. Surprisingly, it is not the root of the tree — it is a leaf: search() lists it first, and walking the parents from .GlobalEnv climbs through the attached packages toward the root:

e <- .GlobalEnv
while (environmentName(e) != environmentName(emptyenv())) {
  print(environmentName(parent.env(e))); e <- parent.env(e)
}
[1] "package:stats"
[1] "package:graphics"
[1] "package:grDevices"
[1] "package:utils"
[1] "package:datasets"
[1] "package:methods"
[1] "Autoloads"
[1] "base"
[1] "R_EmptyEnv"

Attached packages line up as parents, ending at base and finally the empty environment — the one environment with no parent, to which all chains lead.

Environments and Functions

A Function Call Creates an Environment

Calling a function creates a new environment; the arguments are bound to symbols there. (In language lingo: R is lexically scoped.)

env.demo <- function(a, b, c, d) { print(objects()) }
env.demo(1, "truck", c(1,2,3,4,5), pi)
[1] "a" "b" "c" "d"

Only the arguments appear — everything else lives in parent environments. And note carefully: a function’s parent environment is where it was created, not where it is called. For functions you define at the console the two coincide; for functions from packages they differ.

The Call Stack

R also maintains a stack of calling environments (a stack: add and remove only at the top — push and pop, like cafeteria trays). Each call pushes an environment; each return pops one.

Function Description
sys.call the current function call, as a language object
sys.frame the calling environment
sys.nframe number of the current frame (0 at the console)
sys.function the function being evaluated
sys.parent number of the parent frame
sys.calls / sys.frames / sys.parents calls, environments, parents for the whole stack
sys.on.exit the on.exit expression of the current frame
sys.status sys.calls + sys.parents + sys.frames in one list
parent.frame sys.frame(sys.parent(n)) — the parent frame

Why care? A package function sometimes needs a symbol’s meaning in the calling context. Modeling functions do exactly this: a formula contains symbol names but no data, and when you omit the data argument, the function hunts for the variables in the calling environment.

Evaluating in Another Environment: eval

eval evaluates an expression in an arbitrary environment:

eval(expr, envir = parent.frame(),
     enclos = if (is.list(envir) || is.pairlist(envir))
                parent.frame() else baseenv())

The book’s example: a timing wrapper that records the start time, evaluates its argument in the caller’s frame, and prints the elapsed time:

timethis <- function(...) {
  start.time <- Sys.time()
  eval(..., sys.frame(sys.parent(sys.parent())))
  end.time <- Sys.time()
  print(end.time - start.time)
}

The Timing Demo

Time an inefficient function that grows a vector element by element (console transcript):

create.vector.of.ones <- function(n) {
  return.vector <- NA
  for (i in 1:n) {
    return.vector[i] <- 1
  }
  return.vector
}
timethis(returned.vector <- create.vector.of.ones(10000))
## Time difference of 1.485959 secs
length(returned.vector)
## [1] 10000

The timing is nice; the point is the last line — the assignment happened in the calling environment, so returned.vector exists there afterward. (Aside: pre-allocating with length(return.vector) <- n before the loop cuts the time from ~1.5 s to ~0.04 s — a preview of Chapter 24.)

Shorthands: evalq, eval.parent, local

Three convenient wrappers around eval:

  • evalq(expr, ...) ≡ eval(quote(expr), ...) — quote for me;
  • eval.parent(expr, n) ≡ eval(expr, parent.frame(n)) — evaluate in the caller;
  • local(expr) ≡ eval(quote(expr), envir=new.env()) — evaluate in a fresh environment.

eval.parent shortens the timing function nicely:

timethis.b <- function(...) {
  start.time <- Sys.time()
  eval.parent(...)
  end.time <- Sys.time()
  print(end.time - start.time)
}

Data as Environment: with and within

Treating a data frame or list as an environment lets you use its element names like symbols:

example.list <- list(a=1, b=2, c=3)
a + b + c                          # not in the workspace...
Error:
! 找不到物件 'a'
with(example.list, a + b + c)      # ...but with() finds them
[1] 6
within(example.list, d <- a + b + c)
$a
[1] 1

$b
[1] 2

$c
[1] 3

$d
[1] 6

with(data, expr) evaluates and returns the result; within(data, expr) modifies and returns the data.

attach and detach

attach adds the objects of a “database” — a list, data frame, or saved data file — to the search path; detach removes them:

attach(what, pos = 2, name = deparse(substitute(what)),
       warn.conflicts = TRUE)
detach(name, pos = 2, unload = FALSE)

pos is the search-path position, name labels the attached database (used again by detach), warn.conflicts warns about masked names, and unload controls namespace unloading.

Warning

Use attach sparingly. With several data frames sharing column names, it becomes genuinely hard to tell which object a name refers to. Prefer transform to change columns and with to evaluate expressions against a data frame.

Exceptions

Errors and Warnings

Invalid expressions produce errors; questionable ones produce warnings:

12 / "hat"
Error in `12 / "hat"`:
! 二元運算子中有非數值引數
if (c(TRUE, FALSE)) TRUE else FALSE
Error in `if (c(TRUE, FALSE)) ...`:
! 條件的長度 > 1

(The second was merely a warning in the book’s era; modern R promotes it to an error.) Why discuss exceptions in the environments chapter? Because when an exception occurs, the interpreter may abandon the current function and signal the exception in the calling environment — exception handling and environments are intertwined.

Signaling: stop, warning, message

Three levels of telling the user something, from fatal to informative:

doWork <- function(filename) {
  if (file.exists(filename)) read.delim(filename)
  else stop("Could not open the file: ", filename)
}
doWork("file that doesn't exist")
Error in `doWork()`:
! Could not open the file: file that doesn't exist
doNoWork <- function(filename) {
  if (file.exists(filename)) "la la la"
  else warning("File does not exist: ", filename)
}
doNoWork("another file that doesn't exist")
Warning in doNoWork("another file that doesn't exist"): File does not exist:
another file that doesn't exist
doNothing <- function(x) {
  message("This function does nothing.")
}
doNothing("another input value")
This function does nothing.

In your own programs: stop on errors that invalidate the work; warning for problems the caller should know about; message for chatter.

Catching: try

Suppose foo calls bar, bar sometimes fails (can’t open a file, say), and foo wants to survive and try something else. The simple tool is try, which hides most of the machinery:

res <- try({x <- 1}, silent=TRUE)
res
[1] 1
res <- try({open("file that doesn't exist")}, silent=TRUE)
res
[1] "Error in UseMethod(\"open\") : \n  沒有適用的方法可將 'open' 套用到 \"character\" 類別的物件\n"
attr(,"class")
[1] "try-error"
attr(,"condition")
<simpleError in UseMethod("open"): 沒有適用的方法可將 'open' 套用到 "character" 類別的物件>
class(res)
[1] "try-error"

try(expr, silent) evaluates expr; on failure it returns an object of class "try-error" (and prints the message unless silent=TRUE).

Catching: tryCatch

The capable tool is tryCatch, taking an expression, a set of handlers, and a final expression:

tryCatch(expression, handler1, handler2, ..., finally=finalexpr)

Evaluation order: R evaluates expression; if a condition (error or warning) arises, R picks the handler whose argument matches the condition’s class; afterward finally is evaluated — with the handlers no longer active. A taste:

tryCatch({
  log(-1)
}, warning = function(w) "caught a warning",
   finally = message("done either way"))
[1] "caught a warning"