20.3 Quosures

Almost every use of eval() involves both an expression and environment. This coupling is so important that we need a data structure that can hold both pieces. Base R does not have such a structure75 so rlang fills the gap with the quosure, an object that contains an expression and an environment. The name is a portmanteau of quoting and closure, because a quosure both quotes the expression and encloses the environment. Quosures reify the internal promise object (Section 6.5.1) into something that you can program with.

In this section, you’ll learn how to create and manipulate quosures, and a little about how they are implemented.

20.3.1 Creating

There are three ways to create quosures:

  • Use enquo() and enquos() to capture user-supplied expressions. The vast majority of quosures should be created this way.

    foo <- function(x) enquo(x)
    foo(a + b)
    #> <quosure>
    #> expr: ^a + b
    #> env:  global
  • quo() and quos() exist to match to expr() and exprs(), but they are included only for the sake of completeness and are needed very rarely. If you find yourself using them, think carefully if expr() and careful unquoting can eliminate the need to capture the environment.

    quo(x + y + z)
    #> <quosure>
    #> expr: ^x + y + z
    #> env:  global

  • new_quosure() create a quosure from its components: an expression and an environment. This is rarely needed in practice, but is useful for learning, so is used a lot in this chapter.

    new_quosure(expr(x + y), env(x = 1, y = 10))
    #> <quosure>
    #> expr: ^x + y
    #> env:  0x557cade62588

20.3.2 Evaluating

Quosures are paired with a new evaluation function eval_tidy() that takes a single quosure instead of an expression-environment pair. It is straightforward to use:

q1 <- new_quosure(expr(x + y), env(x = 1, y = 10))
eval_tidy(q1)
#> [1] 11

For this simple case, eval_tidy(q1) is basically a shortcut for eval(get_expr(q1), get_env(q1)). However, it has two important features that you’ll learn about later in the chapter: it supports nested quosures (Section 20.3.5) and pronouns (Section 20.4.2).

20.3.3 Dots

Quosures are typically just a convenience: they make code cleaner because you only have one object to pass around, instead of two. They are, however, essential when it comes to working with ... because it’s possible for each argument passed to ... to be associated with a different environment. In the following example note that both quosures have the same expression, x, but a different environment:

f <- function(...) {
  x <- 1
  g(..., f = x)
}
g <- function(...) {
  enquos(...)
}

x <- 0
qs <- f(global = x)
qs
#> <list_of<quosure>>
#> 
#> $global
#> <quosure>
#> expr: ^x
#> env:  global
#> 
#> $f
#> <quosure>
#> expr: ^x
#> env:  0x557caea41b70

That means that when you evaluate them, you get the correct results:

map_dbl(qs, eval_tidy)
#> global      f 
#>      0      1

Correctly evaluating the elements of ... was one of the original motivations for the development of quosures.

20.3.4 Under the hood

Quosures were inspired by R’s formulas, because formulas capture an expression and an environment:

f <- ~runif(3)
str(f)
#> Class 'formula'  language ~runif(3)
#>   ..- attr(*, ".Environment")=<environment: R_GlobalEnv>

An early version of tidy evaluation used formulas instead of quosures, as an attractive feature of ~ is that it provides quoting with a single keystroke. Unfortunately, however, there is no clean way to make ~ a quasiquoting function.

Quosures are a subclass of formulas:

q4 <- new_quosure(expr(x + y + z))
class(q4)
#> [1] "quosure" "formula"

which means that under the hood, quosures, like formulas, are call objects:

is_call(q4)
#> [1] TRUE

q4[[1]]
#> Warning: Subsetting quosures with `[[` is deprecated as of rlang 0.4.0
#> Please use `quo_get_expr()` instead.
#> This warning is displayed once per session.
#> `~`
q4[[2]]
#> x + y + z

with an attribute that stores the environment:

attr(q4, ".Environment")
#> <environment: R_GlobalEnv>

If you need to extract the expression or environment, don’t rely on these implementation details. Instead use get_expr() and get_env():

get_expr(q4)
#> x + y + z
get_env(q4)
#> <environment: R_GlobalEnv>

20.3.5 Nested quosures

It’s possible to use quasiquotation to embed a quosure in an expression. This is an advanced tool, and most of the time you don’t need to think about it because it just works, but I talk about it here so you can spot nested quosures in the wild and not be confused. Take this example, which inlines two quosures into an expression:

q2 <- new_quosure(expr(x), env(x = 1))
q3 <- new_quosure(expr(x), env(x = 10))

x <- expr(!!q2 + !!q3)

It evaluates correctly with eval_tidy():

eval_tidy(x)
#> [1] 11

However, if you print it, you only see the xs, with their formula heritage leaking through:

x
#> (~x) + ~x

You can get a better display with rlang::expr_print() (Section 19.4.7):

expr_print(x)
#> (^x) + (^x)

When you use expr_print() in the console, quosures are coloured according to their environment, making it easier to spot when symbols are bound to different variables.

20.3.6 Exercises

  1. Predict what each of the following quosures will return if evaluated.

    q1 <- new_quosure(expr(x), env(x = 1))
    q1
    #> <quosure>
    #> expr: ^x
    #> env:  0x557cb0b7d2f8
    
    q2 <- new_quosure(expr(x + !!q1), env(x = 10))
    q2
    #> <quosure>
    #> expr: ^x + (^x)
    #> env:  0x557cb0ca8e20
    
    q3 <- new_quosure(expr(x + !!q2), env(x = 100))
    q3
    #> <quosure>
    #> expr: ^x + (^x + (^x))
    #> env:  0x557cb1537430
  2. Write an enenv() function that captures the environment associated with an argument. (Hint: this should only require two function calls.)


  1. Technically a formula combines an expression and environment, but formulas are tightly coupled to modelling so a new data structure makes sense.↩︎