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?
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?
Share Improve this question asked Feb 17 at 12:58 const-aeconst-ae 2,20617 silver badges13 bronze badges 5 |2 Answers
Reset to default 2This 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
I have figured out a decent solution: The trick is to squash the quo
in eval_with_mtcars
and explicitly set the environment of the quo
to the environment of the dots
from wrap_in_tibble
.
wrap_in_tibble <- function(...){
dots <- rlang::enquos(...)
# Let's assume I can be sure that the environment is the same across dots
eval_in_mtcars(tibble::tibble(!!! dots), env = rlang::quo_get_env(dots[[1]]))
}
eval_in_mtcars <- function(expr, env){
quo <- rlang::enquo(expr)
quo <- rlang::new_quosure(rlang::quo_squash(quo), env)
rlang::eval_tidy(quo, data = mtcars[1:3,])
}
wrap_in_tibble(mpg * 3)
#> # A tibble: 3 × 1
#> `mpg * 3`
#> <dbl>
#> 1 63
#> 2 63
#> 3 68.4
I am a bit surprised this works because this means that tibble_quos
is somehow clever enough to realize that the environment of the individual columns should is masked by the mtcars
data. I would still be curious to hear if there is a more official and documented solution to this problem.
wrap_in_tibble <- function(...) { dots <- enquos(...) evaluated_exprs <- lapply(dots, function(quo) { eval_tidy(quo, data = mtcars[1:3,]) }) names(evaluated_exprs) <- vapply(dots, quo_name, character(1)) do.call(tibble, evaluated_exprs) }
– Tim G Commented Feb 17 at 14:55rlang::exprs
breaks in other problematic ways:wrap_in_tibble2 <- function(...){ dots <- rlang::exprs(...) eval_in_mtcars(tibble::tibble(!!! dots)) } fnc <- function(){ a <- 700 wrap_in_tibble2(mpg * a, cyl + 3) } a <- 0 fnc()
– const-ae Commented 2 days ago