Skip to Content
Course content

126: Understanding R Environments Deeply

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring theme when I mentor developers moving from languages like Python or JavaScript into R: they treat the "Global Environment" as the only place that matters, and they assume that when a function looks for a variable, it just checks "where it is right now."

The Myth: Functions search for variables where they are called

Many learners believe that R uses dynamic scoping. In their minds, if a function references a variable z that isn't defined inside the function, R will look at the environment of the code that called the function. It feels intuitive—like a relay race where the caller hands off the context to the function.

Let's look at why that's wrong. Run this in your console:

# Define a variable in the global environment
multiplier <- 2

# Create a function that uses that variable
calculate_area <- function(radius) {
  return(pi * (radius^2) * multiplier)
}

# Now, let's create a new environment and put a DIFFERENT multiplier in it
custom_env <- new.env()
custom_env$multiplier <- 10

# Assign the function to that environment
assign("calculate_area", calculate_area, envir = custom_env)

# Call the function from the custom environment
# If R looked 'where it was called', it might see the global multiplier (2)
# or the custom_env multiplier (10). Let's see.
custom_env$calculate_area(1)

If you expected this to use the multiplier from custom_env (10), you'll be surprised. It returns 6.283... (pi * 1^2 * 2). It ignored the 10 entirely. Why? Because the function doesn't care where it is called; it only cares where it was defined.

The Truth: R uses Lexical Scoping

R uses lexical scoping. This means a function carries its "birth certificate" with it. When calculate_area was created, it was born in the Global Environment. Therefore, its parent environment is the Global Environment. Whenever it can't find a variable locally, it doesn't look at the caller; it looks at its parent.

I like to think of environments as a linked list of dictionaries. Each environment has a pointer to its parent. When R looks for a symbol, it searches the current environment; if it's not there, it follows the pointer to the parent, then the grandparent, all the way up to the emptyenv().

In the example above, calculate_area was defined in the Global Environment. When we moved the function into custom_env, we moved the function object, but we didn't change its internal pointer to its parent. It still points to the Global Environment where multiplier <- 2 lives.

Navigating the Environment Chain

To really master this, you need to stop thinking of "the environment" as a single thing and start thinking about the chain. You can actually inspect this using parent.env().

If you create a nested environment, the chain gets interesting:

env_a <- new.env(parent = .GlobalEnv)
env_b <- new.env(parent = env_a)

assign("x", 10, envir = env_a)
assign("y", 20, envir = env_b)

# env_b can see y (local) and x (from parent env_a)
get("x", envir = env_b) # Returns 10

# env_a cannot see y (it's in the child)
get("y", envir = env_a) # Error: object 'y' not found

This hierarchy is exactly how R handles packages. When you load a library, R adds that package's environment to the search path. When you call a function, R walks that chain. If you've ever accidentally masked a base R function by naming your own variable sum or df, you've experienced the environment chain in action—your local environment was searched first, and R found your variable before it ever reached the package:base environment.

A quick tip: if you ever find yourself in a debugging nightmare wondering "Which version of this variable is R actually using?", use ls(envir = ...)` or parent.env()` to trace the lineage. Don't guess; trace the pointers.




📋 Practical Task

Fixing the Shadowed Configuration in a Nested Environment

You are debugging a system where a "Config" environment is supposed to hold global settings, but a local function is ignoring the updates. Your task is to fix the update_setting function so that it modifies the variable in the parent environment, not the local one.

Copy this broken code into your console:

# Setup
config_env <- new.env()
config_env$timeout <- 30

# This function is intended to update the timeout in the config_env
update_setting <- function(new_value) {
  # BUG: This creates a local variable 'timeout' instead of updating the parent
  timeout <- new_value 
}

# We attach the function to the config environment
assign("update_setting", update_setting, envir = config_env)

# Testing the bug
config_env$update_setting(60)
print(config_env$timeout) # Still returns 30!

Your Goal: Rewrite the update_setting function using assign() and parent.env() so that calling config_env$update_setting(60) correctly changes config_env$timeout to 60.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.