How to use enquos on a list of bares instead of dots?

Howdy!

I'm trying to write a function that will accept a list of bares.

This is easy to do with dots, but in my real use case I want to save dots for passing arguments elsewhere.

Here is a reprex showing the idea, first how this is easy to do with dots, but then how it fails with a list.


suppressPackageStartupMessages(library(tidyverse))
suppressPackageStartupMessages(library(rlang))

# enquos on dots works easily
expr_then_dots <- function(df, q1, ...){
  
  q1 <- enexpr(q1)
  q2 <- enquos(...)
  
  df %>% 
    filter(!!q1) %>% 
    select(!!!q2) %>% 
    head(3)
  
}

#yay!
expr_then_dots(mtcars, cyl>3, mpg, cyl)
#>    mpg cyl
#> 1 21.0   6
#> 2 21.0   6
#> 3 22.8   4

#but enquos on a list does not work 
expr_then_list <- function(df, q1, q2){
  
  q1 <- enexpr(q1)
  q2 <- enquos(q2)
  
  df %>% 
    filter(!!q1) %>% 
    select(!!!q2) %>% 
    head(3)
  
}

# oh no
expr_then_list(mtcars, cyl>3, list(mpg, cyl))
#> Error in .f(.x[[i]], ...): object 'cyl' not found

Created on 2019-06-14 by the reprex package (v0.3.0)

My best guess here is I think the enquos is maybe capturing the list command? But I can't unlist bares I don't think... Any help on how to use enquos() on a list of bares in a function would be great!

Cheers,
Ben

You could use vars() instead of list() and then use select_at(). Would that be sufficient for what you are trying to do?

No need to enquos() if using vars().

expr_then_list <- function(df, q1, q2){
    
    q1 <- enexpr(q1)
    #q2 <- enquos(q2)
    
    df %>% 
        filter(!!q1) %>% 
        select_at(q2) %>% 
        head(3)
    
}

expr_then_list(mtcars, cyl>3, vars(mpg, cyl))

   mpg cyl
1 21.0   6
2 21.0   6
3 22.8   4
3 Likes

Follow-up question @aosmith : Would this approach, i.e., using vars(), be the "correct" tidyeval way to pass any number of grouping variables (including none) into a function without using the dots argument for the bare column names? For example, should I be writing my functions as follows when I want to allow the user to choose any number of grouping columns, including none:

fnc = function(data, groups=NULL) {
  data %>% 
    group_by_at(groups) %>% 
    tally
}

fnc(mtcars)
fnc(mtcars, vars(cyl))
fnc(mtcars, vars(cyl, vs))

This approach also allows the user to do fnc(mtcars, c("cyl", "vs")), which seems like a nice benefit as well.

The way I understand tradeoff between dots and vars is that often times you'll need two sets of dots. For example, when using mutate_at, dots are already "taken" by the function you are passing in and its arguments. Therefore, if you still want to be flexible in passing in any number of variables in, you have to use something like vars.

For that reason, if you don't have dots that are already taken by something else, it's always a good idea to just pass dots in since then you don't need to do anything, but pass dots further into group_by/mutate or whatever. This will change what you can pass into the function, of course. You'll no longer be able to use selection (i.e., c, starts_with etc.), but from a user's perspective you can always make sure to mention whether this function is based around actions or selections (https://resources.rstudio.com/rstudio-conf-2019/working-with-names-and-expressions-in-your-tidy-eval-code).

2 Likes

Well, I'm not an expert but I think this is a correct way to pass things if you don't want to use dots. I saw a comment on this SO answer from Lionel Henry that I read to mean using dplyr::vars() like this is appropriate (but could have misinterpreted).

I've also used alist() instead of list() for these kinds of tasks (also mentioned in that comment), but it didn't seem to work quite right in this scenario.

1 Like