Facilities to capture, inspect, manipulate, and create lazy values (promises), "..." lists, and active calls.
nseval is the missing API for non-standard evaluation and
metaprogramming in R.
nseval might be for you if:
match.call, and other cases of non-standard evauation;
install.packages("devtools") library(devtools) install_github("crowding/nseval")
nseval introduces two S3 classes:
mirror R's promises and
..., respectively. Unlike their
counterparts, these are ordinary data objects, and can be assigned to
variables and be manipulated without triggering evaluation.
quotationcombines an R expression with an environment. There are also
forcedquotations, which pair an expression with a value.
dotsis a named list of quotations.
There is a set of consistently-named accessors and constructors for capturing, constructing, and manipulating these objects.
quo. This captures the environment along with the text of its argument.
dots()captures multiple arguments, analagously to
arg(x), which captures the argument's environment along with its text.
dots(...), to capture
...unevaluated, including original environments, or
arg_list(x, y, (...)), to include other arguments as well.
get_call, which preserves the environment attached to each argument.
do, which allows different arguments to be passed from different environments.
arg_env, which gives the environment attached to an argument, which is what you actually want most the time, or
caller, which returns the calling environment, like
parent.frameoften does, but avoids the latter's difficulties with lazy evaluation and closures;
callerwould rather throw an error than return an incorrect result.
Before R, there was S, and S had some metaprogramming facilities,
exposed by functions like
eval, and so on. R duplicated that
API. But S did not have lexical scoping, closures, or the notion of an
environment, whereas R has all those things.
In S, lazily evaluated arguments could be evaluated simply by stepping
one step up in the call stack and evaluating them in that context.
This is not the case with R, because environments can come from
different sources via
..., drop off the stack, and then be
re-activated via closures (and these situations happen frequently
enough in everyday code).
So R has been coping with a metaprogramming API that was not designed with R's capabilities in mind. Because the S interface is not sufficient to model R behavior, we end up with consequences such as:
match.call()loses information about argument scopes, so normally occurring function calls often can't be captured in a reproducible form;
do.callcan't reproduce many situations that occur in normal evaluation in R;
parent.frame()tells you something almost but not entirely unlike what you actually need to know in most situations;
...rapidly turns painful;
and so on. As a result, R functions that use the S metaprogramming API often end up with unintended behaviors that don't "fit" R: they lose track of variable scope, suffer name collisions, are difficult to compose, etc.
The good news is that you can simply replace most uses of
do.call and such with their
nseval, and may then have fewer of these kinds of
nseval doesn't implement quasiquotation or hygeinic macros or code
coverage or DSLs or interactive debugging. But it is intended to be a
solid foundation to build those kinds of tools on! Watch this space.
nseval doesn't introduce any fancy syntax -- the only nonstandard
evaluation in its own interface is name lookup and quoting, and
standard-evaluating equivalents are always also present.
nseval doesn't try and remake all of R's base library, just the parts
about calls and lazy evaluation.
nseval has no install dependencies and should play well with base R or any
Some other packages have tread similar ground:
It turns out that R's implementation of lazy evaluation via "promise" objects amount to a recreation of fexprs. On the topic of how to work with fexprs, particularly in combination with lexical scope and environments, John Shutt's 2010 PhD thesis has been helpful.
Initial CRAN Release.