Search code examples
rrlang

Explanation of rlang operators used to write functions


I recently posted two questions (1, 2) related to functions I was trying to write. I received useful answers to each, which resulted in the following two functions:

second_table <- function(dat, variable1, variable2){
 dat %>% 
  tabyl({{variable1}}, {{variable2}}, show_na = FALSE) %>% 
  adorn_percentages("row") %>% 
  adorn_pct_formatting(digits = 1) %>% 
   adorn_ns() 

 }

And

second_table2 = function(dat, variable1, variable2){
  variable1 <- sym(variable1)
  
  dat %>% 
    tabyl(!!variable1, {{variable2}}, show_na = FALSE) %>% 
    adorn_percentages("row") %>% 
    adorn_pct_formatting(digits = 1) %>% 
    adorn_ns() 
  
}

These functions work as intended, but I had never used the rlang package before and am still confused about the difference between the {{}} operator and !! + sym() after looking through the available documentation and writing some additional functions. I don't like to use code that I don't fully understand and am sure I will have further use for these rlang operators in the future, so would greatly appreciate a plain-language explanation of what the difference is between these operators.


Solution

  • R has a particular feature called non-standard evaluation (NSE), where expressions are used as-is instead of being evaluated. Most people first encounter NSE when they load packages:

    a <- "rlang"
    
    print(a)         # Standard evaluation - the expression a is evaluated to its value
    # [1] "rlang"
    
    library(a)       # Non-standard evaluation - the expression a is used as-is
    # Error in library(a) : there is no package called ‘a’
    

    rlang enables sophisticated NSE by providing three main functions to capture unevaluated symbols and expressions:

    • sym("x") captures a symbol (i.e., variable name, column name, etc.). Older versions allowed for sym(x), but I think the latest version of rlang forces the input to be a string.

    • expr(a + b) captures arbitrary expressions

    • quo(a + b) captures arbitrary expressions AND the environment where these expression were defined.

    The difference between expressions and quosures is that evaluating the former will be done in the immediate environment, while the latter is always evaluated in the environment where the expression was captured:

    f <- function(e) {a <- 2; b <- 3; eval_tidy(e)}
    a <- 5; b <- 10
    
    f(expr(a+b))   # Evaluated inside f
    # [1] 5
    
    f(quo(a+b))    # Evaluated in the environment where it is captured
    # [1] 15
    

    All three verbs have en-equivalents: ensym, enexpr and enquo. These are used to capture symbols and expressions provided to a function from within that function. This is useful when you want to remove the need for a user of the function to use sym, etc. themselves:

    f <- function(x) {enexpr(x)}      # Expression captured within a function
    f(a+b)
    
    # This has exact equivalence to
    
    f <- function(x) {x}
    f(expr(a+b))                      # The user has to do the capture themselves
    

    In all cases, the operator !! evaluates symbols and expressions. Think of it as eval() on steroids, because !! forces immediate evaluation that takes precedence over everything else. Among other things, this can be useful for iterative construction of more complicated expressions:

    a <- expr(b + 2)
    expr(d * !!a)      # a is evaluated immediately
    # d * (b + 2)
    
    expr(d * eval(a))  # evaluation of a is delayed
    # d * eval(a)
    

    With all that said, {{x}} is shorthand notation for !!enquo(x)