Search code examples
rdplyrnsetidyeval

Why is enquo + !! preferable to substitute + eval


In the following example, why should we favour using f1 over f2? Is it more efficient in some sense? For someone used to base R, it seems more natural to use the "substitute + eval" option.

library(dplyr)

d = data.frame(x = 1:5,
               y = rnorm(5))

# using enquo + !!
f1 = function(mydata, myvar) {
  m = enquo(myvar)
  mydata %>%
    mutate(two_y = 2 * !!m)
}

# using substitute + eval    
f2 = function(mydata, myvar) {
  m = substitute(myvar)
  mydata %>%
    mutate(two_y = 2 * eval(m))
}

all.equal(d %>% f1(y), d %>% f2(y)) # TRUE

In other words, and beyond this particular example, my question is: can I get get away with programming using dplyr NSE functions with good ol' base R like substitute+eval, or do I really need to learn to love all those rlang functions because there is a benefit to it (speed, clarity, compositionality,...)?


Solution

  • I want to give an answer that is independent of dplyr, because there is a very clear advantage to using enquo over substitute. Both look in the calling environment of a function to identify the expression that was given to that function. The difference is that substitute() does it only once, while !!enquo() will correctly walk up the entire calling stack.

    Consider a simple function that uses substitute():

    f <- function( myExpr ) {
      eval( substitute(myExpr), list(a=2, b=3) )
    }
    
    f(a+b)   # 5
    f(a*b)   # 6
    

    This functionality breaks when the call is nested inside another function:

    g <- function( myExpr ) {
      val <- f( substitute(myExpr) )
      ## Do some stuff
      val
    }
    
    g(a+b)
    # myExpr     <-- OOPS
    

    Now consider the same functions re-written using enquo():

    library( rlang )
    
    f2 <- function( myExpr ) {
      eval_tidy( enquo(myExpr), list(a=2, b=3) )
    }
    
    g2 <- function( myExpr ) {
      val <- f2( !!enquo(myExpr) )
      val
    }
    
    g2( a+b )    # 5
    g2( b/a )    # 1.5
    

    And that is why enquo() + !! is preferable to substitute() + eval(). dplyr simply takes full advantage of this property to build a coherent set of NSE functions.

    UPDATE: rlang 0.4.0 introduced a new operator {{ (pronounced "curly curly"), which is effectively a short hand for !!enquo(). This allows us to simplify the definition of g2 to

    g2 <- function( myExpr ) {
      val <- f2( {{myExpr}} )
      val
    }