20 Evaluation

20.1 Prerequisites

20.2 Evaluation basics

  1. Q: Carefully read the documentation for source(). What environment does it use by default? What if you supply local = TRUE? How do you provide a custom environment?

    A: By default, source() uses the global environment. A specific evaluation environment may be chosen, by passing it to the local argument. To use current environment (i.e. the calling environment of source()) set local = TRUE.

  2. Q: Predict the results of the following lines of code:

    A: Let’s look at a quote from the first edition of Advanced R:

    expr() and eval() are opposites. […] each eval() peels off one layer of expr()’s”.

    In general eval(expr(x)) evaluates to x. Therefore, (1) evaluates to \(2 + 2 = 4\). Adding another eval() doesn’t have impact here. So, also (2) evaluates to 4. However, when wrapping (1) into expr() the whole expression will be quoted.

  3. Q: Fill in the function bodies below to re-implement get() using sym() and eval(), and assign() using sym(), expr(), and eval(). Don’t worry about the multiple ways of choosing an environment that get() and assign() support; assume that the user supplies it explicitly

    A: We reimplement these two functions using tidy evaluation. We turn the string name into a symbol, then evalute it:

    To build the correct expression for the value assignment, we unquote using !!.

  4. Q: Modify source2() so it returns the result of every expression, not just the last one. Can you eliminate the for loop?

    A: In order to highlight the modifications, we’ve preserved the code from the former source2()-function in a comment.

    Let’s create a file and test source2(). Keep in mind that <- returns invisibly.

    To eliminate the for loop, we may also use map():

  5. Q: We can make base::local() slightly easier to understand by spreading it over multiple lines:

    Explain how local() works in words. (Hint: you might want to print(call) to help understand what substitute() is doing, and read the documentation to remind yourself what environment new.env() will inherit from.)

    A: Let’s follow the advice and add print(call) inside of local3():

    The first line generates a call to eval(). Because substitute() operates in the current evaluation argument. However, this doesn’t matter here, as both, expr and envir are promises and therefore “the expression slots of the promises replace the symbols”, from ?substitute.

    Next, call will be evaluated in the caller environment (aka the parent frame). Given that call contains another call eval() why does this matter? The answer is subtle: this outer environment determines where the bindings for eval, quote, and new.env are found.

20.3 Quosures

  1. Q: Predict what evaluating each of the following quosures will return if evaluated.

    A: Each quosure will be evaluated in its own environment so each x is bound to a different value. This leads us to:

  2. Q: Write an enenv() function that captures the environment associated with an argument. (Hint: this should only require two function calls.)

    A: A quosure captures both the expression and the environment. From a quosure, we can access the environment with the help of get_env().

20.4 Data masks

  1. Q: Why did I use a for loop in transform2() instead of map()? Consider transform2(df, x = x * 2, x = x * 2).

    A: A for loop applies the processing steps regarding .data iteratively. This includes updating .data and reusing the same variable names. This makes it possible to apply transformations sequentially, so that subsequent transformations can refer to columns that were just created.

  2. Q: Here’s an alternative implementation of subset2():

    Compare and contrast subset3() to subset2(). What are its advantages and disadvantages?

    A: Let’s take a closer look at subset2() first:

    subset2() provides an additional logical check, which is missing from subset3(). Here, rows is evaluated in the context of data, which results in a logical vector. Afterwards only [() needs to be used for subsetting.

    With subset3() both of these steps occur in a single line. This means, that the subsetting is also evaluated in the context of the data mask.

    This is shorter (but probably also less readable), because the evaluation and the subsetting take place in the same expression. However, it may introduce unwanted errors, if the data mask contains an element named “data”, as the objects from the data mask take precedence over arguments of the function.

  1. Q: The following function implements the basics of dplyr::arrange(). Annotate each line with a comment explaining what it does. Can you explain why !!.na.last is strictly correct, but omitting the !! is unlikely to cause problems?

    A: arrange2() basically reorders a data frame by one or more of its variables. As arrange2() allows to provide the variables as expressions (via ...), these need to be quoted first. Afterwards they are used to build up an order() call, which is then evaluated in the context of the data frame. Finally, the data frame is reordered via integer subsetting. Let’s take a closer look at the source code:

    By using !!.na.last the .na.last argument is unquoted, when the order() call is built. This way, the na.last argument is already correctly specified (typically TRUE, FALSE or NA).

    Without the unquoting, the expression would read na.last = .na.last and the value for .na.last would still need to be looked up and found. Because these computations take place inside of the functions execution environment (which contains .na.last), this is unlikely to cause problems.

20.5 Using tidy evaluation

  1. Q: I’ve included an alternative implementation of threshold_var() below. What makes it different to the approach I used above? What makes it harder?

    A: Lets compare this approach to the original implementation:

    We can see, that threshold_var2() no longer coerces the symbol to a string. Therefore $ instead of [[ can be used for subsetting. Initially we suspected partial matching would work with $, but .data deliberately avoids this problem.

    The prefix call to $() is less common than infix-subsetting using [[, but ultimately both functions behave the same.

20.6 Base evaluation

  1. Q: Why does this function fail?

    A: In this function, lm_call is evaluated in the caller environment, which happens to be the global environment. In this environment, the name data is bound to utils::data. To fix the error, we can either set the evaluation environment to the functions execution environment or unquote the data argument when building the call to lm().

    When we want to unquote an argument within a function, we first need to capture the user-input (by enenxpr()).

  2. Q: When model building, typically the response and data are relatively constant while you rapidly experiment with different predictors. Write a small wrapper that allows you to reduce duplication in the code below.

    A: In our wrapper lm_wrap(), we provide mpg and mtcars as default response and data. This seems to give us a good mix of usability and flexibility.

  3. Q: Another way to write resample_lm() would be to include the resample expression (data[sample(nrow(data), replace = TRUE), , drop = FALSE]) in the data argument. Implement that approach. What are the advantages? What are the disadvantages?

    A: We can take advantage of the lazy evaluation of function arguments, by moving the resampling step into the argument definition. The uses passes the data to the function, but only a permutation of this data (rsampled_data) will be used.

    With this approach the evaluation needs to take place within the functions environments, because the resampled dataset (defined as a default argument) will only be available in the function environment.

    Overall, putting an essential part of the preprocessing outside of the functions body is not common practice in R. Compared to the unquoting-implementation (resample_lm1()), this approach captures the model-call in a more meaningful way.