Evaluating Promises and Calls in R: A Deep Dive
In R, promises and calls are fundamental concepts that enable functional programming. Understanding how these concepts interact with each other is crucial for effective coding and debugging.
When a promise is turned into a call using the substitute() function, it’s essential to understand what happens to the evaluation environment (envir). This post will delve into the details of how this process works and explore the implications on code execution.
Introduction
In R, promises are objects that hold an expression and an envir. When you create a promise using the promise() function or by wrapping an expression in parentheses, it’s stored in memory with both the expression and the envir associated with it. This allows the environment to be evaluated when the promise is resolved.
library(pryr)
a_promise <- promise(function() condition) # Create a promise with an expression
The Substitute Function
The substitute() function is used to convert a promise into a call, which can then be executed using eval(). When you use substitute(), it creates a new call object that points to the original promise but resolves it immediately.
a_call <- substitute(a_promise)
The Call Object
The resulting call object (a_call) is an expression, not a call. It’s essentially a pointer to another promise in the parent frame that hasn’t been resolved yet.
Properties of the Call Object
When you inspect the class() of the call object using class(a_call), it returns "symbol", indicating that it’s a symbol (or function name). However, it doesn’t have the same properties as an actual call or expression. You can verify this by checking its attributes using attr(), which will reveal that there is no envir associated with the call object.
class(a_call) # [1] "symbol"
attr(a_call, "env") # NULL
Evaluating the Call
When you evaluate the call using eval() and pass it an envir (e.g., parent.frame()) and a parent frame, it attempts to resolve the promise associated with the call. This process is different from evaluating the original expression directly.
Evaluation Context
Evaluating the call uses the provided envir and parent frame, which allows it to access the required objects. However, in this case, since a_call points to a resolved promise in the parent frame, R will attempt to evaluate the previous promise instead of executing the new expression.
eval(a_call, df, parent.frame()) # Error: object 'a' not found
The Difference Between eval() and eval()
When you use eval() directly on an expression (eval( a >= 4, df)), R evaluates it in the given envir. However, when you use eval() with a call object (eval(a_call, df, parent.frame())), it attempts to resolve the promise associated with the call.
The key difference lies in how these operations are executed:
- Direct evaluation: The expression is evaluated directly using the provided envir.
- Call resolution: The call object’s promise is resolved immediately, which can lead to unexpected behavior if not handled correctly.
Example Walkthrough
To better understand this process, let’s walk through an example.
library(pryr)
df <- data.frame(a = 1:5, b = 5:1)
# Create a promise with an expression
a_promise <- promise(function() condition)
# Convert the promise to a call
a_call <- substitute(a_promise)
f <- function(df, a_promise) {
print(promise_info(a_promise)) # shows both the expression and the envir
a_call <- substitute(a_promise)
print(a_call) # condition
eval(a_call, df, parent.frame()) # Error: object 'a' not found
}
g <- function(df, condition) {
f(df, condition)
}
# Use the call to resolve its promise
eval( expression(condition), df) # [1] FALSE FALSE FALSE TRUE TRUE.
In this example, f() is called with a call object (a_call) that points to the unresolved promise. However, when we pass the call object directly to eval(), it resolves immediately and returns the result of evaluating the expression in the provided envir.
Conclusion
Understanding how promises and calls interact with each other in R can be complex. The process of converting a promise into a call using substitute() creates a new call object that points to the original promise but resolves it immediately. This resolution is different from direct evaluation, where the expression is evaluated directly using the provided envir.
When working with these concepts, it’s essential to consider the implications on code execution and handle potential edge cases correctly.
Last modified on 2023-09-27