最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

r - Rlang nested quotation fails for tibble but works for data.frame - Stack Overflow

programmeradmin4浏览0评论

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 We have to evaluate each expression separately in the correct environment before creating the tibble: 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:55
  • Thanks. I agree that calling eval_tidy individually would work. But I find it difficult to imagine that there is no direct way to do this. Also, do you know if this behavior is documented anywhere? – const-ae Commented Feb 17 at 15:15
  • Maybe this helps: tibble.tidyverse./reference/tibble.html – Tim G Commented Feb 17 at 20:21
  • 1 Interesting suggestion, but unfortunately using rlang::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
  • 1 I went ahead and re-posted this on the rlang GitHub page: github/r-lib/rlang/issues/1779 – Mikko Marttila Commented yesterday
Add a comment  | 

2 Answers 2

Reset to default 2

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

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.

发布评论

评论列表(0)

  1. 暂无评论