Chapter 6 Advanced Topics

6.1 Non-Standard Evaluation

Non-standard evaluation is a catch-all term that means they don’t follow the usual R rules of evaluation. Instead, they capture the expression that you typed and evaluate it in a custom way.

6.1.1 Strings and quoting

Here’s a simple example in the case of strings. If you want to return a personalised message, it is obvious the below function will not give you what you want:

greet <- function(name) {
  "Hello, name"
}

greet("Bob")
## [1] "Hello, name"

The function returns the string exactly as it is written because it is in quotes. With the glue package you can use non-standard evaluation to return what you want:

greet <- function(name) {
  glue("Hello, {name}")
}

greet("Bob")
## Hello, Bob

The glue() function “unquotes” the “name” and evaluates it as a variable.

6.1.2 Base R evaluation

6.1.2.1 Expressions and quoting

A similar approach can be taken to more general expressions in R. An expression is some R code has not been evaluated yet. It is captured and saved for later. In base R this can be achieved using the quote() function. Expressions can be one of four classes:

  • “constants” (e.g. 4, TRUE).
  • “names” (e.g. variable names), that have type “symbols”.
  • “calls” (e.g. unevaluated function calls), that have type “language”.
  • “pairlists” (not really used anymore).

Constants are not very interesting, so we will concentrate on names and calls. As with strings, we can capture the expression, rather than the evaluated result, using the quote() function:

quote(x) %>% class()
## [1] "name"
quote(mean(1:10)) %>% class()
## [1] "call"

To evaluate a quoted expression use the eval() function:

x <- 10

quote(x) %>% eval()
## [1] 10
quote(mean(1:10)) %>% eval()
## [1] 5.5

6.1.2.2 Names and Environments

An environment is a container for variables, binding a set of names to a set of values. Every environment also has a parent environment, except the empty environment, which is the ultimate ancester of all environments. If a name is not found in the current environment R looks in its parent and so on through the generations. This is known as lexical scoping.

When using eval() as above the expression is evaluated in the current environment. However, the environment to evaluate the expression in can be set as a parameter to eval().

Note: data.frames can be treated as environments containing the column names binded to the column vectors.

6.1.2.3 Calls

A call is a delayed evaluation of a function call. The function and the argument names are stored, but not evaluated. So you can change the values associated with the arguments before evaluation. In fact they need not be defined at creation time at all:

wait_for_it <- quote(x + y)

x <- 3
y <- 8

eval(wait_for_it)
## [1] 11

6.1.2.4 Parsing and Deparsing

Parsing turns text into expressions and deparsing turns expressions into text. The parse() function converts text, but its default argument is a file connection, so we must use the text argument:

parse(text = "8 + 10") %>% eval()
## [1] 18

Deparse might be useful for returning information to the user:

friendly_eval <- function(expr) {
  str_c("The value of ", deparse(expr), " is ", eval(expr))
}

quote(8 + 10) %>% friendly_eval()
## [1] "The value of 8 + 10 is 18"

6.1.2.5 Functions and closures

When you execute a function it creates a new, temporary, environment. The named arguments to the function, plus any variables created within its body are stored in this environment. Thus it cannot effect the variables outside its scope.

The parent for the function’s environment is the environment in which the function was created in, not the one in which it was executed in. Thus it has access to all the variables in the parent environment. This is known as closure, as it encloses the parent environment, and can be a powerful tool.

6.1.2.6 Substitute and promise

The friendly_eval() function above requires its argument to be quoted. It would be nice if we could write the expression directly as an argument. However, quote() makes a literal quote of its input, in this case expr. What we need is substitute(). This will lookup all the object names provided to it, and if it finds a value for that name, it will substitute the name for its value:

friendly_eval <- function(expr) {
  expr_sub <- substitute(expr)
  str_c("The value of ", deparse(expr_sub), " is ", eval(expr_sub))
}

friendly_eval(8 + 10)
## [1] "The value of 8 + 10 is 18"

The above function only works because in R function arguments are evaluated lazily – variables are not evaluated until they are used. R stores arguments as a promise, which contains the expression of an argument along with the value. substitute() capture the expression before it is evaluated.

6.1.2.7 Formula and overscoping

A formula is a domain specific language (DSL) to simplify expressing the relationship between variables. Just like functions, formulas enclose the environment they are created in. When a formula is evaluated later in a different environment, it can still access all the objects that lived in its original environment.

If an object exists in more than one accessible environment the enviroment the formula (or function) is evaluated in takes precidence. If the object is not found, R looks in the enclosed environment. This is known as overscoping, as the formula or function has scope beyond its execution environment.

6.1.3 Tidy Evaluation

To summarise the above, the following points are important to tidy evaluation:

  1. quote() delays evaluation of an expression.
  2. eval() evaluates an expression in the current (or a specified) environment.

Functions and formulas:

  1. enclose the environment they were created in.
  2. evaluate objects in their own, and enclosed, environments

Tidy evaluation has two new additions: quasiquotation and quosures.

6.1.3.1 Quasiquotation

Quasiquotation enables the user to evaluate parts of the expression right away, while quoting the rest. Say we have the expression z - x + 4 and we know the value of x at the time of quoting, we can unquote x with the function UQ() or the operator !!. However, quote() will not work in this quasiquotation, we need to use the tidy evaluation equivalent, expr():

x <- 10

expr(z - UQ(x) + 4)
## z - 10 + 4
expr(z - !!x + 4) %>% class()
## [1] "call"

The expr() function returns expressions, as does quote(), without any information on the environment it was created in. Some other useful functions for quasiquotation are:

  • UQS() and the !!! operator unquote and splice their arguments.
  • enexpr() takes an expression, looks up any symbols (names) within it and returns it unevaluated. It is equivalent to substitute().
  • exprs() captures multiple expressions and returns a list.

6.1.3.2 Quosures

As the name suggests, quosures are hybrids of quotes and closures. They are unevaluated expressions that enclose their creation environment. This is very similar to formulas and they are in fact implemented as one-sided formulas. Quosures are created with the quo() function:

quo(z - UQ(x) + 4)
## <quosure>
## expr: ^z - 10 + 4
## env:  000001E0BE02E8E0
quo(z - !!x + 4) %>% class()
## [1] "quosure" "formula"

The quosure equivalent of substitute() is enquo(). It takes a symbol referring to a function argument, quotes the R code that was supplied to this argument, captures the environment where the function was called (and thus where the R code was typed), and bundles them in a quosure.

Another useful function is quos(), it returns a list of quosures. You can supply several expressions directly, e.g.  quos(foo, bar), but more importantly you can also supply dots: quos(...).

Note that quosures don’t make a lower level distinction between calls and names. Every expression becomes a quosure.

To evaluate quosures we need a special function to implement the environment scoping properly. rlang provides such a function: eval_tidy()

x <- 10

quo(x) %>% eval_tidy()
## [1] 10
quo(mean(1:10)) %>% eval_tidy()
## [1] 5.5

6.1.3.3 Parsing and Deparsing

rlang also provides functions for parsing and deparsing expressions. To turn a string into an expression use: - parse_expr(), which works in the same way as parse(text = ...). - parse_quosure() to create a quosure.

To turn an expression into a string, rlang provides two functions: - expr_text() to turn an expression into a string. - expr_label() to produce a string that is more suited to use as a label.

friendly_eval <- function(expr) {
  str_c("The value of ", deparse(expr), " is ", eval(expr))
}

quote(8 + 10) %>% friendly_eval()
## [1] "The value of 8 + 10 is 18"