Skip to Content
Course content

9: Working with Environments

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

I was working on a project last week where I had about fifty different configuration constants—API endpoints, timeout settings, retry limits—all sitting in my global environment. It was a mess. I kept accidentally overwriting a variable called timeout that was being used by another part of my script, and it took me twenty minutes to realize why my network requests were suddenly failing.

The Global Space is Getting Crowded

Usually, when we create a variable in R, we're just throwing it into the Global Environment. It's convenient, but it's like keeping all your tools in one giant pile on the floor. Let's try to isolate some of these settings. I'll start by creating a dedicated environment for my configuration.

# Let's make a specific container for our config
my_config <- new.env()

# Now I'll put some settings in there
my_config$api_url <- "https://api.example.com/v1"
my_config$timeout <- 30

# If I try to call timeout directly, what happens?
print(timeout)
# Error: object 'timeout' not found

That's exactly what I wanted. The timeout variable is tucked away inside my_config, so it doesn't collide with anything else in my workspace. I can access it via the $ operator, which is basically me saying, "Go look inside this specific box."

Wait, why can it still see my Global variables?

Here is where things get interesting. I noticed something strange while debugging. If I create a variable in the global space and then try to access it through my environment, R behaves in a way that feels a bit like magic—or a bug, depending on your mood.

# I'll create a global variable
global_setting <- "I am global"

# Now I'll try to get it from my_config
print(my_config$global_setting)
# [1] "I am global"

Wait. I never put global_setting inside my_config. Why is it returning the value? This is the core of how R environments work: lexical scoping. Every environment has a "parent." When you ask an environment for a value it doesn't have, it doesn't just give up. It asks its parent. If the parent doesn't have it, it asks the grandparent, all the way up to the Global Environment and eventually the base package.

Controlling the Search Path

If this parent-child relationship is what's causing the lookup, I wonder if I can break it or redirect it. Let's say I want a "Production" config and a "Development" config, where the Development one inherits from Production but can override specific values.

# The base production settings
prod_env <- new.env()
prod_env$port <- 80
prod_env$debug <- FALSE

# Now, let's make a dev environment that uses prod as its parent
dev_env <- new.env(parent = prod_env)

# I'll override just the debug setting for dev
dev_env$debug <- TRUE

# Now look what happens when we query dev_env
print(dev_env$debug) # [1] TRUE (Found in dev_env)
print(dev_env$port)  # [1] 80   (Not in dev_env, found in parent prod_env)

This is actually incredibly powerful. I've essentially created a hierarchy. By setting the parent argument in new.env(), I'm defining the search path. If I want to completely isolate an environment so it can't see anything outside itself, I can set the parent to an empty environment: new.env(parent = emptyenv()).

The Danger of assign() and get()

While the $ notation is great, you'll often see assign() and get() in professional R code. I used to avoid them because they felt clunky, but they are necessary when the name of the variable you're looking for is itself stored in another variable (dynamic naming).

# Let's say the user tells us which setting they want to change
setting_name <- "timeout"
new_value <- 60

# I can't do my_config$setting_name <- 60 because that creates a 
# variable literally named 'setting_name'.
assign(setting_name, new_value, envir = my_config)

# Now let's retrieve it dynamically
print(get(setting_name, envir = my_config))
# [1] 60

Using assign and get allows us to treat environments like key-value stores (dictionaries), which is often how I handle complex state management in larger R applications.




📋 Practical Task

Building a Hierarchical Plugin Configuration System

You are building a system where a "Core" configuration is defined, but individual "Plugins" can have their own specific settings that override the core while still inheriting everything else.

Your Task:

  • Create an environment called core_config. Give it two variables: version = "1.0.0" and log_level = "INFO".
  • Create a second environment called plugin_config. Set its parent to be core_config.
  • In plugin_config, override the log_level to be "DEBUG".
  • Add a new variable to plugin_config called plugin_name = "DataCleaner".
  • Verify your work by printing the value of version and log_level accessed through plugin_config. (You should see the core version but the plugin's debug level).
  • Finally, use the get() function to retrieve the plugin_name from the plugin_config environment using a character string.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.