Search code examples
rtidyversequoterlangquasiquotes

What does !! operator mean in R


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


Solution

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