I am trying to evaluate some user-provided arguments in a specific data environment using the rlang
quasi-quotation approach. In addition, I want to wrap the output in a data.frame
/ tibble
. However, when I use tibble
the code fails. Here is a minimal reproducible example:
wrap_in_df <- function(...){
dots <- rlang::enquos(...)
eval_in_mtcars(data.frame(!!! dots))
}
wrap_in_tibble <- function(...){
dots <- rlang::enquos(...)
eval_in_mtcars(tibble::tibble(!!! dots))
}
eval_in_mtcars <- function(expr){
quo <- rlang::enquo(expr)
rlang::eval_tidy(quo, data = mtcars[1:3,])
}
wrap_in_df(mpg * 2, cyl + 3)
#> X.mpg...2 X.cyl...3
#> 1 42.0 9
#> 2 42.0 9
#> 3 45.6 7
wrap_in_tibble(mpg * 2, cyl + 3)
#> Error: object 'mpg' not found
Created on 2025-02-17 with reprex v2.1.1
The problem appears when the tibble in the tibble_quos
function calls eval_tidy
on the mpg * 2
argument without providing the mtcars
data.
I remember reading at some point about problems of nesting multiple quosure evaluations, but cannot find this. I know that I could use something like quo_squash
in eval_in_mtcars
, but that has its own set of problems.
Is there some clever invocation that allows me to use a tibble in combination with quasi-evaluation?
This happens because tibble()
captures its arguments as quosures and
evaluates them in its own data mask
(which is the tibble being constructed sequentially). You have no way to
modify that mask, so you need to instead inject your own data mask into the
quosures’ chain of environments.
One way to do that is substitute()
the ...
in to an uneavaluated
expression of a call to tibble()
and then evaluate that expression
with the data in place:
tibble_with_mtcars <- function(...) {
eval(substitute(tibble::tibble(...)), head(mtcars), parent.frame())
}
tibble_with_mtcars(mpg * 2, cyl + 3)
#> # A tibble: 6 × 2
#> `mpg * 2` `cyl + 3`
#> <dbl> <dbl>
#> 1 42 9
#> 2 42 9
#> 3 45.6 7
#> 4 42.8 9
#> 5 37.4 11
#> 6 36.2 9
… but unfortunately this is also fragile:
foo <- function(x) {
tibble_with_mtcars(mpg + {{ x }})
}
foo(wt)
#> Error: object 'wt' not found
A more comprehensive approach would be to go ahead and slap the mask into the environment chain of the quosure, including nested quosures. That could look something like the following, but this is bound to be quite slow at the R level and is certainly not tested thoroughly.
A helper to do that:
quo_mask <- function(quo, mask, recursive = TRUE) {
if (!rlang::is_call(quo)) {
return(quo)
}
if (!rlang::is_environment(mask)) {
mask <- rlang::as_environment(mask)
}
# Insert mask at the bottom of the environment chain.
if (rlang::is_quosure(quo)) {
env <- rlang::quo_get_env(quo)
env <- rlang::env_clone(mask, env)
quo <- rlang::quo_set_env(quo, env)
}
# Iterate through the expression, modifying in place.
if (recursive) {
x <- quo
while (!rlang::is_null(x)) {
car <- rlang::node_car(x)
car <- quo_mask(car, mask)
rlang::node_poke_car(x, car)
x <- rlang::node_cdr(x)
}
}
quo
}
And the application:
tibble_with_mtcars <- function(...){
dots <- rlang::enquos(...)
eval_with_mtcars(tibble::tibble(!!!dots))
}
eval_with_mtcars <- function(expr) {
quo <- rlang::enquo(expr)
quo <- quo_mask(quo, head(mtcars))
rlang::eval_tidy(quo)
}
tibble_with_mtcars(mpg * 2)
#> # A tibble: 6 × 1
#> `mpg * 2`
#> <dbl>
#> 1 42
#> 2 42
#> 3 45.6
#> 4 42.8
#> 5 37.4
#> 6 36.2
foo(wt)
#> # A tibble: 6 × 1
#> `mpg + wt`
#> <dbl>
#> 1 23.6
#> 2 23.9
#> 3 25.1
#> 4 24.6
#> 5 22.1
#> 6 21.6
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With