What does !! operator mean in R

Viewed 401

Can anybody explain, please, what for do we need !!, !!! or {{}} operators from rlang? I tried to learn more about quasiquotation but did not get anything.

I've reached several posts on curly-curly operator on Stack and understood that we use {{ when we are passing dataframe's variables (or other sub-objects of our objects) into the function. But after reading about quote/unquote I was completely confused about all of these operators and their usage.

Why do we need it, why some functions do not read arguments with out it, and, finally, how do they actually work?

I will appreciate if you put the answer in the most simple way that even I will understand (maybe with examples?).

2 Answers

The !! and {{ operators are placeholders to flag a variable as having been quoted. They are usually only needed if you intend to program with the tidyverse. The tidyverse likes to leverage NSE (non-standard Evaluation) in order to reduce the amount of repetition. The most frequent application is towards the "data.frame" class, in which expressions/symbols are evaluated in the context of a data.frame before searching other scopes. In order for this to work, some special functions (like in the package dplyr) have arguments that are quoted. To quote an expression, is to save the symbols that make up the expression and prevent the evaluation (in the context of tidyverse they use "quosures", which is like a quoted expression except it contains a reference to the environment the expression was made). While NSE is great for interactive use, it is notably harder to program with. Lets consider the dplyr::select

 library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
 
 iris <- as_tibble(iris)
 
 my_select <- function(.data, col) {
   select(.data, col) 
 }
 
 select(iris, Species)
#> # A tibble: 150 × 1
#>    Species
#>    <fct>  
#>  1 setosa 
#>  2 setosa 
#>  3 setosa 
#>  4 setosa 
#>  5 setosa 
#>  6 setosa 
#>  7 setosa 
#>  8 setosa 
#>  9 setosa 
#> 10 setosa 
#> # … with 140 more rows
 my_select(iris, Species)
#> Error: object 'Species' not found

we encounter an error because within the scope of my_select the col argument is evaluated with standard evaluation and cannot find a variable named Species.

If we attempt to create a variable in the global environemnt, we see that the funciton works - but it isn't behaving to the heuristics of the tidyverse. In fact, they produce a note to inform you that this is ambiguous use.

 Species <- "Sepal.Width"
 my_select(iris, Species)
#> Note: Using an external vector in selections is ambiguous.
#> ℹ Use `all_of(col)` instead of `col` to silence this message.
#> ℹ See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.
#> This message is displayed once per session.
#> # A tibble: 150 × 1
#>    Sepal.Width
#>          <dbl>
#>  1         3.5
#>  2         3  
#>  3         3.2
#>  4         3.1
#>  5         3.6
#>  6         3.9
#>  7         3.4
#>  8         3.4
#>  9         2.9
#> 10         3.1
#> # … with 140 more rows

To remedy this, we need to prevent evaluation with enquo() and unquote with !! or just use {{.

 my_select2 <- function(.data, col) {
   col_quo <- enquo(col)
   select(.data, !!col_quo) #attempting to find whatever symbols were passed to `col` arugment
 }
 #' `{{` enables the user to skip using the `enquo()` step.
 my_select3 <- function(.data, col) {
   select(.data, {{col}}) 
 }
 
 my_select2(iris, Species)
#> # A tibble: 150 × 1
#>    Species
#>    <fct>  
#>  1 setosa 
#>  2 setosa 
#>  3 setosa 
#>  4 setosa 
#>  5 setosa 
#>  6 setosa 
#>  7 setosa 
#>  8 setosa 
#>  9 setosa 
#> 10 setosa 
#> # … with 140 more rows
 my_select3(iris, Species)
#> # A tibble: 150 × 1
#>    Species
#>    <fct>  
#>  1 setosa 
#>  2 setosa 
#>  3 setosa 
#>  4 setosa 
#>  5 setosa 
#>  6 setosa 
#>  7 setosa 
#>  8 setosa 
#>  9 setosa 
#> 10 setosa 
#> # … with 140 more rows

In summary, you really only need !! and {{ if you are trying to apply NSE programatically or do some type of programming on the language.

!!! is used to splice a list/vector of some sort into arguments of some quoting expression.

 library(rlang)
 quo_let <- quo(paste(!!!LETTERS))
 quo_let
#> <quosure>
#> expr: ^paste("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L",
#>           "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
#>           "Z")
#> env:  global
 eval_tidy(quo_let)
#> [1] "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"

Created on 2021-08-30 by the reprex package (v2.0.1)

Non-standard evaluation (NSE) often gets used together with tidyverse/dplyr, but most people encounter it on a daily basis when they load packages.

a <- "rlang"

print(a)               # Standard evaluation: the expression a is replace by 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’

So, how do you load a dynamically specified package? Here, we will use quasiquotation for demonstration. (In real code, I recommend doing library(a, character.only=TRUE) instead.)

In base R, you can use bquote() to dynamically construct an expression and then evaluate it.

myexpr <- bquote(library(.(a)))      # myexpr will now be library("rlang")
eval(myexpr)                         # rlang is now loaded

rlang provides additional tools to manipulate expressions. In general, they allow you to be more expressive than the base R tools. The !! behaves similarly to the above:

myexpr <- rlang::expr(library(!!a))  # Same as above, myexpr is now library("rlang")

You can use rlang::expr with !! to construct any expressions for future evaluation.

x <- rlang::expr(mtcars)
y <- rlang::expr(mpg > 30)
z <- rlang::expr(disp)
rlang::expr(subset(!!x, !!y, !!z))   # Constructs subset(mtcars, mpg > 30, disp)

When you have a lot of arguments, you can put them in a list and use the !!! shortcut. The above expression can be replicated with

l <- rlang::exprs(mtcars, mpg > 30, disp)   # Note the s on exprs
rlang::expr(subset(!!!l))                   # Also builds subset(mtcars, mpg > 30, disp)

The {{ operator is the most complicated one to explain and requires an introduction of quosures.

Expressions in R are first-class objects, which means that they can be passed into functions, returned by functions, etc. However, expressions created with rlang::expr are always evaluated in their immediate context. Consider,

a <- 10
x <- rlang::expr(a+5)

f <- function(y) {
  a <- 5
  eval(y)
}

f(x)     # What does this return?

Even though the expression x captures a+5, the value of a changes right before the expression is evaluated. Quosures capture expressions AND the environments where they are defined. That environment is always used to evaluate that expression.

a <- 10
x <- rlang::quo(a+5)    # Quosure = expression + environment where a == 10

f <- function(y) {
  a <- 5
  eval_tidy(y)          # Instead of simple eval()
}

f(x)                    # 15 = 10 + 5

Capturing an expression or a quosure can be moved to be inside the function by using the en- versions of expr and quo:

f <- function(y) {
  a <- 5
  eval(rlang::enexpr(y))
}

g <- function(y) {
  a <- 5
  eval_tidy(rlang::enquo(y))
}

allowing users to pass expressions directly to the function

a <- 10
f(a*4)    # 20 = 5*4,  because f captures expressions, and a is overwritten
g(a*4)    # 40 = 10*4, because g captures quosures

And with all of the above said, {{x}} is just a shorthand notation for !!enquo(x).

Related