rlang 0.2.0

  r-lib, rlang

  Lionel Henry

We are happy to announce a new version of rlang that features many improvements to tidy evaluation:

  • The quirks in the quasiquotation syntax have been much reduced.

  • Quosures gained a much improved printing method with colour support.

  • The performance of quoting, splicing and quosure evaluation was vastly improved.

  • Many bugs have been fixed.

Install the latest version of rlang with:

install.packages("rlang")

Syntax adjustments for the !! operator

The headline improvement is a much improved syntax for the !! operator (pronounced bang-bang). In a way bang-bang is a lie, or a polite fiction. It is not a real R operator and only works in tidy evaluation functions. To implement !!, we simply look at the code and reinterpret two consecutive ! negations as the unquoting operator. The major downside of this approach was that our !! operator inherited from the operation precedence of !, which is very low. As a result !!a + b was interpreted by R as !!(a + b) rather than (!!a) + b. To work around this, tidy eval users had to add explicit parentheses, which was often confusing.

Happily this is no longer necessary. We now reprocess the whole syntax tree to give !! a more natural operation precedence, i.e. that of unary + and -. This means that expressions like !!a > b now do the expected thing (only a is unquoted):

a <- sym("foo")
expr(!!a > b)
#> foo > b

In addition, we automatically strip one layer when !! is wrapped in parentheses. This is useful for unquoting function names cleanly:

expr((!!a)(bar, baz))
#> foo(bar, baz)

op <- sym(">")
expr((!!op)(bar, baz))
#> bar > baz

A new print method for quosures

Problems with base R deparsing

When R code is printed at the console (be it a function, an expression, a formula or a quosure), R runs the function deparse() to transform the code to a printable string. The deparsing mechanism in base R is not optimal for tidy eval:

  • It adds redundant parentheses between the ! of !!:

    quote(!!x)
    #> !(!x)
    

    If you ever encounter these weird !(!x) statements (for instance by printing the code of a tidy eval function in the console), it’s because the R printer adds the pair of parentheses at printing-time. The good news is that it is merely a printing problem.

  • The way it prints inlined vectors is ambiguous. R expressions can contain actual data structures not just symbolic code. It is especially easy to inline vectors and other objects with quasiquotation:

    expr(mean(!!c(1, 2, 3)))
    #> mean(c(1, 2, 3))
    

    Note how the output above is indistinguishable from the following output:

    expr(mean(c(1, 2, 3)))
    #> mean(c(1, 2, 3))
    

    In both cases, the R printer outputs the code that creates the vector, even if the vector is already created as in the first case!

  • Quosures are currently implemented as formulas but that’s an implementation detail. Quosures and formulas behave differently and it is misleading to have them print as formulas:

    expr(mean(!!quo(c(1, 2, 3))))
    #> mean(~c(1, 2, 3))
    

    Furthermore, it would be nice to have a way to distinguish quosures coming from different contexts.

Deparsing with rlang

rlang now features expr_print() powered by expr_deparse(). These functions are used in the print method of quosures and nicely solve the printing issues:

  • Inlined vectors are represented with angular brackets and are abbreviated to avoid taking too much space:

    quo(list(code = 1:10, data = !!(1:10)))
    #> <quosure>
    #>   expr: ^list(code = 1:10, data = <int: 1L, 2L, 3L, 4L, 5L, ...>)
    #>   env:  global
    

    S3 objects are printed based on the extensible pillar::type_sum() generic:

    quo(lm(!!(disp ~ cyl), data = !!mtcars))
    #> <quosure>
    #>   expr: ^lm(<S3: formula>, data = <data.frame>)
    #>   env:  global
    
  • As you can see in the output above quosures are now displayed with a leading ^ instead of a tilde in order to distinguish them from formulas. In addition, they are now colourised according to their environments. Quosures from the global environment are always printed with the default colour. Quosures from local environments are printed with a unique colour. To illustrate this, let’s create two quosures referring to arg, where arg represents a different object:

    make_quo <- function(arg) {
      quo(arg)
    }
    
    q1 <- make_quo("foo")
    q2 <- make_quo("bar")
    q3 <- quo(list(!!q1, !!q2))
    
    q3
    #> <quosure>
    #>   expr: ^list(^arg, ^arg)
    #>   env:  global
    

    q3 contains two nested quosures that both point to objects called arg. While they have the same names these objects come from different contexts, the contexts that were created each time we called the function make_quo(). Let’s evaluate q3 to verify this:

    eval_tidy(q3)
    #> [[1]]
    #> [1] "foo"
    #> 
    #> [[2]]
    #> [1] "bar"
    

    The colours should help you figure out what is happening when quosures from disparate contexts are unquoted in a larger expression.

Note that if you are ever confused by how a quosure prints out (perhaps because of those intervening ^ symbols), you can use the new function quo_squash() to get rid of all the nested quosures and print a naked version:

quo_squash(q3)
#> list(arg, arg)

Finally, raw expressions created by expr() are still printed with the base R deparser. If you would like to debug tidy eval code and inspect the result of quasiquotation, use the new qq_show() function to display a raw expression with the new printer:

my_wrapper <- function(data, expr) {
  expr <- enquo(expr)
  qq_show(data %>% mutate(mean(!!expr)))
}

my_wrapper(mtcars, cyl)
#> data %>% mutate(mean(^cyl))