7 Environments

7.1 Environment basics

  1. Q: List three ways in which an environment differs from a list.

    A: The most important differences between environments and lists are:
    • environments have reference semantics
    • environments have parents
    • environments are not ordered
    • elements of environments need to be (uniquely) named
  2. Q: Create an environment as illustrated by this picture.

    A: Let’s create an environment, that contains itsself.

    e1 <- env()
    e1$loop <- e1
  3. Q: Create a pair of environments as illustrated by this picture.

    A: These two environments contain each other:

    e1 <- env()
    e2 <- env()
    e1$loop   <- e2
    e2$dedoop <- e1
  4. Q: Explain why e[[1]] and e[c("a", "b")] don’t make sense when e is an environment.

    A: The first option doesn’t make sense, because elements of an environment are not ordered. The second option would return two objects at the same time without being contained in another data structure. Therefore, it would be unclear how R should handle this type of output.

  5. Q: Create a version of env_poke() that will only bind new names, never re-bind old names. Some programming languages only do this, and are known as single assignment languages.

    A: We want env_poke2() to test, if the supplied name is already present in the given environment. We only allow new names to be assigned to a value, otherwise an (informative) error is thrown.

    env_poke2 <- function (env, nm, value){
      current_names <- env_names(env)
      if (nm %in% current_names) {
        abort(paste0("\"", nm, "\" is already assigned to a value."))
      else {
        assign(nm, value, envir = env)
    # Test
    env_1 <- env(a = 1)
    env_poke2(env_1, "b", 2)
    #> [1] "a" "b"
    env_poke2(env_1, "b", 2)
    #> Warning: `rlang__backtrace_on_error` is no longer experimental.
    #> It has been renamed to `rlang_backtrace_on_error`. Please update your RProfile.
    #> This warning is displayed once per session.
    #> Error: "b" is already assigned to a value.
  6. Q: What does this function do? How does it differ from <<- and why might you prefer it?

    rebind <- function(name, value, env = caller_env()) {
      if (identical(env, empty_env())) {
        stop("Can't find `", name, "`", call. = FALSE)
      } else if (env_has(env, name)) {
        env_poke(env, name, value)
      } else {
        rebind(name, value, env_parent(env))
    rebind("a", 10)
    #> Error: Can't find `a`
    a <- 5
    rebind("a", 10)
    #> [1] 10

    A: The function does “more or less” the same as <<-. rebind() provides an additional env argument (but this functionality is already coverd by assign()). More importantly, rebind() will only carry out an assignment when it finds a binding in one of the parent environments of env. This is different than the behaviour of <<- (see textbook):

    If <<- doesn’t find an existing variable, it will create one in the global environment. This is usually undesirable, because global variables introduce non-obvious dependencies between functions.

7.2 Recursing over environments

  1. Q: Modify where() to return all environments that contain a binding for name. Carefully think through what type of object the function will need to return.

    A: The modified function will always recurse until it reaches the empty environment. Along the way, it will check each environment for a given name. Only if no matching object is found in any environment, an error will be thrown. Otherwise the environments containing matching objects will be written to a list, which will be returned once the function terminates. Please also note how the list is initialized via the default argument, when the function is called for the first time.

    where2 <- function(name, env = caller_env(), env_list = list()) {
      if (identical(env, empty_env())) {
        if (length(env_list)){
          # Final case
        } else {
          # Base case
          stop("Can't find ", name, call. = FALSE)
      } else {
        if(env_has(env, name)){
          # Success case
          env_list <- c(env_list, env)
        # Recursive case
        where2(name, env_parent(env), env_list)
    # Test
    e1a <- env(empty_env(), a = 1, b = 2)
    e1b <- env(e1a, b = 10, c = 11)
    e1c <- env(e1b, a = 12, d = 13)
    where2("a", e1c)
    #> [[1]]
    #> <environment: 0x65183c0>
    #> [[2]]
    #> <environment: 0x64532f0>
  2. Q: Write a function called fget() that finds only function objects. It should have two arguments, name and env, and should obey the regular scoping rules for functions: if there’s an object with a matching name that’s not a function, look in the parent. For an added challenge, also add an inherits argument which controls whether the function recurses up the parents or only looks in one environment.

    A: We follow a similar approach to the previous exercise. This time we additionally check if the found object is a function and implement and argument to turn off the recursion, if desired.

    fget2 <- function(name, env = caller_env(), inherits = TRUE){
      # browser()
      if (identical(env, emptyenv())) {
        # Base case
        stop("Could not find function called \"", name, "\"", call. = FALSE) 
      else if (name %in% env_names(env) && is.function(env[[name]])) {
        # Success case
      else if (!inherits) {
        # Escape case ;-)
        stop("Could not find function called \"", name, "\"",
             "\nConsider setting `inherits = TRUE`.",
             call. = FALSE) 
      else {
        # Recursive Case
        fget2(name, env_parent(env))
    # Test
    fget2("filter", inherits = TRUE)
    #> [1] "package:stats"

7.3 Special environments

  1. Q: How is search_envs() different fo env_parents(global_env())?

    A: search_envs() returns all the environments on the search path. “The search path is a chain of environments containing exported functions of attached packages” (from ?search_envs). Every time you attach a new package, this search path will grow. The search path ends with the base-environment. The global environment is included, because functions present in the global environment will always be part of the search path.

    #>  [[1]] $ <env: global>
    #>  [[2]] $ <env: package:rlang>
    #>  [[3]] $ <env: tools:rstudio>
    #>  [[4]] $ <env: package:stats>
    #>  [[5]] $ <env: package:graphics>
    #>  [[6]] $ <env: package:grDevices>
    #>  [[7]] $ <env: package:utils>
    #>  [[8]] $ <env: package:datasets>
    #>  [[9]] $ <env: package:methods>
    #> [[10]] $ <env: Autoloads>
    #> [[11]] $ <env: package:base>

    env_parents(global_env()) will list all the ancestors of the global environment, therefore the global environment itsself is not included. This also includes the “ultimate ancestor”, the empty environment. This environment is not part of the search path, because it contains no objects would need to be found.

    #>  [[1]] $ <env: package:rlang>
    #>  [[2]] $ <env: tools:rstudio>
    #>  [[3]] $ <env: package:stats>
    #>  [[4]] $ <env: package:graphics>
    #>  [[5]] $ <env: package:grDevices>
    #>  [[6]] $ <env: package:utils>
    #>  [[7]] $ <env: package:datasets>
    #>  [[8]] $ <env: package:methods>
    #>  [[9]] $ <env: Autoloads>
    #> [[10]] $ <env: package:base>
    #> [[11]] $ <env: empty>
  2. Q: Draw a diagram that shows the enclosing environments of this function:

    f1 <- function(x1) {
      f2 <- function(x2) {
        f3 <- function(x3) {
          x1 + x2 + x3

    A: Each function environment binds its parent environment. The function environments contain functions and the values provided in the function call.

  3. Q: Write an enhanced version of str() that provides more information about functions. Show where the function was found and what environment it was defined in.

    A: Apart from the requested features, let us also provide the function type (see ?pryr::ftype for details). We use functions from the pryr package, since it provides helpers for all requested features:

    fstr <- function(obj){
      if (!is.function(obj)) {stop("fstr works only for functions")}
      obj_str <- lazyeval::expr_text(obj)
      flist <- list(ftype = pryr::ftype(obj),
                    where = pryr::where(obj_str),
                    enclosing_env = pryr::enclosing_env(obj),
                    args = pryr::fun_args(obj))
    # Test
    #> Registered S3 method overwritten by 'pryr':
    #>   method      from
    #>   print.bytes Rcpp
    #> $ftype
    #> [1] "primitive" "generic"  
    #> $where
    #> <environment: base>
    #> $enclosing_env
    #> NULL
    #> $args
    #> NULL

    We chose to use non-standard evaluation for the input just like str() does. pryr::where() requires character input. We catch the name of the supplied object and use lazyeval::expr_text() (https://github.com/hadley/lazyeval) to create the string we need.

7.4 The call stack

  1. Q: Write a function that lists all the variables defined in the environment in which it was called. It should return the same results as ls(). A: We can implement this dynamic scoping behaviour, by explicitly referencing the caller environment. Pleaso note, that this approach returns also variables starting with a dot - an option we also pass to ls().

    ls2 <- function(env = caller_env()) {
    # Test in global environment
    ls(all.names = TRUE)
    #>  [1] ".Random.seed" "a"            "e1"           "e1a"         
    #>  [5] "e1b"          "e1c"          "e2"           "env_1"       
    #>  [9] "env_poke2"    "error_wrap"   "fget2"        "fstr"        
    #> [13] "ls2"          "rebind"       "where2"
    #>  [1] "e1b"          "e1c"          "env_poke2"    "fget2"       
    #>  [5] "rebind"       "where2"       "a"            "error_wrap"  
    #>  [9] "env_1"        "e1"           "e2"           ".Random.seed"
    #> [13] "ls2"          "fstr"         "e1a"
    # Test in "sandbox" environment
    e1 <- env(a = 1, b = 2)
    invoke(ls, .env = e1)
    #> [1] "a" "b"
    invoke(ls2, .env = e1)
    #> [1] "a" "b"