Recursive Evaluation in R: Mastering rapply, evalq, Substitute and more

Recursive Evaluation in the Global Environment

In this article, we will delve into the world of recursive evaluation and explore why rapply does not work as expected when trying to evaluate expressions in the global environment. We will also discuss the nuances of lapply, how to work around the issues with rapply, and provide examples to illustrate our points.

Understanding Rapply

rapply is a function in R that applies a function to each element of a list or vector. It differs from lapply in its behavior, particularly when it comes to evaluating arguments before calling the C code.

The documentation for rapply states that:

“The semantics differ in detail from lapply: in particular the arguments are evaluated before calling the C code.”

This means that when using rapply, the arguments are evaluated immediately, rather than being passed as a list of values to be evaluated later. This can lead to unexpected behavior and errors.

Example: rapply with stop(“error”)

To illustrate this point, let’s consider an example using rapply. We’ll try to apply a function that stops R with the message “error”.

rapply(list(quote(stop("error"))), function(x) x)
# Error in (function (x)  : error

lapply(list(quote(stop("error"))), function(x) x)
# [[1]]
# stop("error")

As we can see, rapply fails with an error message, while lapply succeeds and returns the expected result.

Recursive Evaluation in rapply

Now that we’ve explored how rapply works, let’s dive into our original question. We have a nested list of expressions and want to evaluate them recursively using rapply. However, we’re having trouble getting x to change in the global environment.

# Some expressions   
expr1 <- quote(x <- x + 9)
expr2 <- quote(x <- x/2)
expr3 <- quote(x <- x + 3)
expr4 <- quote(x <- x * 3)

# Generate a nested list of expressions
exprs <- list(expr1, list(expr2, expr3), expr4)

# Initialize x, and attempt to eval the expressions
x <- 1
rapply(exprs, eval, envir = globalenv())
# Returns: [1] 10  5  8 24, but x is not changed.

We’re using rapply with eval as our function, and passing the global environment as the environment for evaluation. However, we’re not getting the expected results.

Workaround: Using evalq

As suggested in the answer to our question, we can use evalq instead of eval. This allows us to evaluate expressions using a function that takes an expression as input, rather than applying a fixed function to each element of a list.

rapply(exprs, evalq, envir = globalenv())  # updated with Hadley's equivalent but cleaner version.
# [1] 10  5  8 24

x
# [1] 24

As we can see, rapply now works as expected, and x is changed in the global environment.

Substitute: Salvaging the Original Expression

However, there’s still a problem. The actual expression being evaluated is not our original expression, but rather the result of evaluating 10 in the global environment. This is because evalq evaluates the expression using the current value of x, which we’ve set to 24.

To salvage the original expression, we can use the substitute function, which allows us to replace variables with their values.

substitute(expr1)
# expr1

By using substitute, we can replace the variable x in our original expressions with its current value. This ensures that our final results are accurate and consistent.

Flatten: A Utility Function for List Manipulation

In order to use lapply or rapply effectively, we need to flatten the list of expressions into a single list. We can do this using a utility function called flatten.

flatten <- function(x) {
  repeat {
    if(!any(vapply(x, is.list, logical(1)))) return(x)
    x <- Reduce(c, x)
  }
}

# Generate a nested list of expressions
exprs <- list(expr1, list(expr2, expr3), expr4)

# Flatten the list of expressions
flattened_exprs <- flatten(exprs)

By using flatten, we can easily transform our nested list of expressions into a single list that we can evaluate using lapply or rapply.

Conclusion

In this article, we’ve explored why rapply does not work as expected when trying to evaluate expressions in the global environment. We’ve also discussed how to use evalq and substitute to salvage our original expressions and achieve the desired results.

Additionally, we’ve introduced a utility function called flatten, which allows us to transform nested lists of expressions into single lists that can be evaluated using lapply or rapply.

By understanding these nuances and using the right tools for the job, you’ll be able to work effectively with recursive evaluation in R.


Last modified on 2024-02-14