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()
andenquos()
to capture user-supplied expressions. The vast majority of quosures should be created this way.function(x) enquo(x) foo <-foo(a + b) #> <quosure> #> expr: ^a + b #> env: global
quo()
andquos()
exist to match toexpr()
andexprs()
, but they are included only for the sake of completeness and are needed very rarely. If you find yourself using them, think carefully ifexpr()
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:
new_quosure(expr(x + y), env(x = 1, y = 10))
q1 <-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:
function(...) {
f <- 1
x <-g(..., f = x)
} function(...) {
g <-enquos(...)
}
0
x <- f(global = x)
qs <-
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:
~runif(3)
f <-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:
new_quosure(expr(x + y + z))
q4 <-class(q4)
#> [1] "quosure" "formula"
which means that under the hood, quosures, like formulas, are call objects:
is_call(q4)
#> [1] TRUE
1]]
q4[[#> 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.
#> `~`
2]]
q4[[#> 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:
new_quosure(expr(x), env(x = 1))
q2 <- new_quosure(expr(x), env(x = 10))
q3 <-
expr(!!q2 + !!q3) x <-
It evaluates correctly with eval_tidy()
:
eval_tidy(x)
#> [1] 11
However, if you print it, you only see the x
s, 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
Predict what each of the following quosures will return if evaluated.
new_quosure(expr(x), env(x = 1)) q1 <- q1#> <quosure> #> expr: ^x #> env: 0x557cb0b7d2f8 new_quosure(expr(x + !!q1), env(x = 10)) q2 <- q2#> <quosure> #> expr: ^x + (^x) #> env: 0x557cb0ca8e20 new_quosure(expr(x + !!q2), env(x = 100)) q3 <- q3#> <quosure> #> expr: ^x + (^x + (^x)) #> env: 0x557cb1537430
Write an
enenv()
function that captures the environment associated with an argument. (Hint: this should only require two function calls.)
Technically a formula combines an expression and environment, but formulas are tightly coupled to modelling so a new data structure makes sense.↩︎