rtidyverserlangquosure

r rlang: using is_quosure on tidyselect helper


Suppose that you have an argument in a R function that could be either:

  1. a raw tidyselect helper such as contains("a"), starts_with("a") etc,
  2. a list of quosures with the helpers, with vars(contains("a")).

How do you check within the function if you are in case (1) or (2)?

The problem is that is_quosures(vars(contains("a"))) works, but is_quosures(contains("a")) will not work, as it tries first to evaluate the function contains("a"), which returns an error when evaluated alone!?

library(rlang)
library(dplyr)
is_quosures(vars(contains("a")))
#> [1] TRUE
is_quosures(contains("a"))
#> Error: No tidyselect variables were registered

fo <- function(var) {
  is_quosures(var)  
}

fo(vars(contains("a")))
#> [1] TRUE
fo(contains("a"))
#> Error: No tidyselect variables were registered

Created on 2019-12-03 by the reprex package (v0.3.0)

Use case

You want to use a function such as summarise_at(data, var), and want to make it robust to the user specifying var as a direct tidyselect helper, or wrapped within a vars() call. Only way I figured out to handle this is to do a case-by-case if/then checking if it is a quosure or not (then wrap into vars if not), but this will precisely fail in the case above.

library(rlang)
library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union

fo <- function(var) {
  is_var_closure <- rlang::is_quosures(var)
  if(is_var_closure) {
    dplyr::summarise_at(iris, var, mean)  
  } else {
    dplyr::summarise_at(iris, quos(var), mean)  
  }
}


fo(vars(starts_with("Sepal")))
#>   Sepal.Length Sepal.Width
#> 1     5.843333    3.057333
fo(starts_with("Sepal"))
#> Error: No tidyselect variables were registered

Created on 2019-12-03 by the reprex package (v0.3.0)


Solution

  • The way I've done this before is by capturing the expression and checking with is_call:

    f <- function(var) {
      if (rlang::is_call(rlang::enexpr(var), names(tidyselect::vars_select_helpers))) {
        rlang::enquos(var)
      }
      else {
        stopifnot(rlang::is_quosures(var)) # or something more specific with a useful message
        var
      }
    }
    
    # both work
    f(vars(starts_with("Sepal")))
    f(starts_with("Sepal"))
    

    Just make sure to use enexpr for is_call, see this GitHub issue.