Skip to contents

R-CMD-check CRAN version badgeCodecov test coverage

nseval is the missing API for non-standard evaluation and metaprogramming in R.

Who nseval is for

nseval might be for you if:

  • You’ve been befuddled by trying to get your desired results using functions like substitute, eval, parent.frame(), do.call, match.call, and other cases of non-standard evauation;
  • You’re been befuddled by trying to interface with other people’s code that uses the above functions, and need a way to work around them;
  • You want to better understand what “goes on” “under the hood” when R is running.

Installation

nseval is on CRAN, install the latest release with:

install.packages("nseval")

To install the development branch:

install.packages("devtools")
devtools::install_github("crowding/nseval")

What nseval does

nseval introduces two S3 classes: quotation, and dots, which 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.

  • A quotation combines an R expression with an environment. There are also forced quotations, which pair an expression with a value.
  • A dots is a named list of quotations.

There is a set of consistently-named accessors and constructors for capturing, constructing, and manipulating these objects.

Quick intro / transitioning from base R to nseval

  • Instead of quote, use:
    • quo. This captures the environment along with the text of its argument.
    • Similarly, dots() captures multiple arguments, analagously to alist.
  • Instead of substitute(x), use:
    • arg(x), which captures the argument’s environment along with its text.
  • Instead of substitute(list(...))[[2]], or substitute(...()), use:
    • dots(...), to capture ... unevaluated, including original environments, or
    • arg_list(x, y, (...)), to include other arguments as well.
  • Instead of match.call, use:
    • get_call, which preserves the environment attached to each argument.
  • Instead of do.call, use
    • do, which allows different arguments to be passed from different environments.
  • Instead of parent.frame, use:
    • arg_env, which gives the environment attached to an argument (recognizing that this can be different for different arguments!)
    • caller, which returns the calling environment, like parent.frame often does, but avoids the latter’s difficulties with lazy evaluation and closures; caller would rather throw an error than return an incorrect result.

Why nseval is needed

Before R, there was S, and S had some metaprogramming facilities, exposed by functions like parent.frame, substitute, match.call, do.call, quote, alist, 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.call can’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;
  • it’s difficult to wrap or extend nonstandard-evaluating functions;
  • it’s difficult to use a nonstandard-evaluating function as an argument to a higher order function;
  • any mixture of metaprogramming and ... 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 match.call, parent.frame, do.call and such with their equivalents from nseval, and may then have fewer of these kinds of problems.

What nseval doesn’t do

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 other ’verse.

Similar packages

Some other packages have tread similar ground:

Further reading

It turns out that R’s implementation of lazy evaluation via “promise” objects are effectively a recreation of fexprs with lexical scope 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.