Skip to Content
Course content

33: Environments and Closures in R

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

Imagine you're working in a large corporate office. You have your own desk where you keep your stapler, your notebook, and your coffee mug. When you need a pen, you first look on your desk. If it's not there, you don't just give up; you ask your direct supervisor. If your supervisor doesn't have one, they ask their boss, and so on, all the way up to the CEO. The CEO's office is the "Global Environment"—everyone eventually checks there if they can't find what they need anywhere else.

In R, this is exactly how environments and lexical scoping work. An environment isn't just a place where variables live; it's a package that contains both a set of bindings (the variables) and a pointer to a "parent" environment. When you call a variable, R searches the current environment, then the parent, then the grandparent, until it hits the Global Environment or throws an error.

The Chain of Command for Variables

Most of the time, you're working in the Global Environment, so this feels invisible. But once you start writing functions, R creates a new, temporary environment for that function. I like to think of this as the function "renting" a desk for the duration of its execution. Once the function returns a value, that desk is cleared and the environment is usually deleted.

# A simple look at how R searches
parent_env <- new.env()
parent_env$secret_code <- 42

child_env <- new.env(parent = parent_env)
child_env$my_name <- "Dev"

# This finds it in the child environment
print(get("my_name", envir = child_env)) 

# This doesn't exist in child_env, so R looks at parent_env
print(get("secret_code", envir = child_env)) 

Notice how child_env doesn't actually contain secret_code, but it can still "see" it. That's the power of the parent pointer.

Functions with a Memory

Now, here is where things get interesting: Closures. A closure is simply a function that "remembers" the environment in which it was created. If you return a function from inside another function, the inner function carries its birth-environment with it, even after the outer function has finished running.

It's like a manager giving you a specialized toolkit before they retire. Even though the manager is gone, you still have the tools they gave you, and those tools are tied to the specific settings the manager configured for you.

I use this pattern all the time when I want to create "factory" functions—functions that build other functions with specific behaviors without polluting my global workspace with a dozen different variables.

make_multiplier <- function(x) {
  # This environment is created when make_multiplier is called
  function(y) {
    # This inner function 'closes over' the variable x
    return(x * y)
  }
}

# Create a function that always doubles
double <- make_multiplier(2)
# Create a function that always triples
triple <- make_multiplier(3)

print(double(10)) # 20
print(triple(10)) # 30

In the example above, double and triple are closures. They both use the same code, but they point to different environments. double points to an environment where x is 2, and triple points to one where x is 3. This is how we achieve "state" in R without relying on messy global variables.

Creating Persistent State

You can take this a step further by using the <<- operator. While <- creates a variable in the current local environment, <<- tells R to look up the chain of environments until it finds a matching variable to update, or creates it in the global environment if it finds nothing. In a closure, this allows you to create a variable that persists across multiple calls to the function.

make_counter <- function() {
  count <- 0
  function() {
    count <<- count + 1 # Update 'count' in the parent environment
    return(count)
  }
}

my_counter <- make_counter()
print(my_counter()) # 1
print(my_counter()) # 2
print(my_counter()) # 3

If you tried to call print(count) in your console right now, you'd get an error. That's the beauty of closures: the count variable is "private." It's locked inside the environment that my_counter carries around, protected from the rest of your script.




📋 Practical Task

Building a Private Secret-Key Generator

Your goal is to create a system that generates "signer" functions. Each signer should be initialized with a unique secret key. The signer function should then take a message (a string) and return a combined "signed" string, but the secret key itself must remain inaccessible from the Global Environment.

Requirements:

  • Create a function called make_signer that takes one argument: secret_key.
  • make_signer should return an inner function that takes one argument: message.
  • The inner function should return a string in the format: "[Message]: [SecretKey]".
  • Verify your work by creating two different signers (e.g., alice_signer and bob_signer) with different keys and ensuring they produce different signatures for the same message.
  • Try to print the secret_key directly from the global environment to prove it is hidden.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.