What does !! operator mean in R
Asked Answered
B

2

6

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?).

Breastplate answered 30/8, 2021 at 12:44 Comment(5)
"Why do we need it" You need them because the tidyverse heavily uses non-standard evaluation. As someone who doesn't use the tidyverse, I have never used any of them.Aloke
In base R, !! means double- (and !!! triple)-negation of logical operators. rlang and other tidyverse packages have adopted it to be used for NSE evaluation of variables.Ahlgren
This is likely a duplicate but ! being ! makes !! twice as hard to search for.Barram
If you didn’t understand them after reading the chapter on quasi quotation, I’m not sure what more can be said. Maybe the programming with dplyr guide will help: dplyr.tidyverse.org/articles/programming.html.Piston
@Piston well, thank you! Language barrier is the reason why I sometimes do not get the information from the documentation that is written in a very technical and only-programmer-friendly language.Breastplate
O
6

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)

Oxonian answered 30/8, 2021 at 15:17 Comment(1)
Justin, thank you very much for the introduction into NSE. I will accept your answer for this question, but it does not mean that it is better than Artem's. Your answers supplement each other, so they draw a really big and simple picture of using some operators.Breastplate
T
4

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).

Tidy answered 31/8, 2021 at 2:34 Comment(4)
library(a, character.only = TRUE)Aloke
Some things were clarified, thank you for your patience, Артем!Breastplate
Artem, I have accepted Justin's answer but it does not mean that yours did not answer the question. If I could accept two of them, I would have accepted yours too. Thank you!Breastplate
No problem at all. I'm glad it was helpful. I do want to caution that NSE can make code hard to read and maintain. I created a simple example for demonstration here, but in real code, I would use the library() call @Aloke wrote in his comment.Tidy

© 2022 - 2024 — McMap. All rights reserved.