Search code examples
rdplyrquasiquotes

Pass multiple calling arguments to a formal argument in dplyr custom function without using "..."


To make a custom function flexible to receiving one or more calling arguments per formal argument I currently rely on "...":

library(dplyr)

foo <- function(data, ..., dv){
  groups <- enquos(...)
  dv <- enquo(dv)
  data %>% 
    group_by(!!!groups) %>% 
    summarise(group_mean = mean(!!dv))
}

mtcars %>% foo(am, dv = mpg)
mtcars %>% foo(vs, am, dv = mpg)

But "..." obscures the logic of the function, and it could not be used in a custom function with 2 or more formal arguments requiring multiple calling arguments.

Is there a way to write the above function to utilize a formal argument (e.g., "groups") rather than "..." that can receive a single vector name or a vector of vector names as its argument(s)? Something like:

foo <- function(data, groups, dv){
  groups <- enquos(groups)
  dv <- enquo(dv)

  data %>% 
    group_by(!!!groups) %>% 
    summarise(group_mean = mean(!!dv))
}

# Failing code
mtcars %>% foo(groups = c(vs, am), dv = mpg)

Note that this code would work, but require user to remember to use quos() in function body:

foo <- function(data, groups, dv){
  dv <- enquo(dv)

  data %>% 
    group_by(!!!groups) %>% 
    summarise(group_mean = mean(!!dv))
}

mtcars %>% foo(groups = quos(vs, am), dv = mpg)

I'd love to rely on enquos() in body of function instead.


Solution

  • We could place the ... at the end

    foo <- function(data,  dv, ...){
       groups <- enquos(...)
       dv <- enquo(dv)
       data %>% 
         group_by(!!!groups) %>% 
         summarise(group_mean = mean(!!dv))
      }
    

    If we want to pass a vector of 'group', then one option is group_by_at

    foo <- function(data, groups, dv){
      dv <- enquo(dv)
    
      data %>% 
         group_by_at(vars(groups)) %>% 
         summarise(group_mean = mean(!!dv))
      }
    
    mtcars %>% 
        foo(groups = c("vs", "am"), dv = mpg)
    # A tibble: 4 x 3
    # Groups:   vs [?]
    #     vs    am group_mean
    #  <dbl> <dbl>      <dbl>
    #1     0     0       15.0
    #2     0     1       19.8
    #3     1     0       20.7
    #4     1     1       28.4
    

    One option if we want to pass unquoted expression with c would be to convert it to expression and then evaluate it

    foo <- function(data, groups, dv){
    
     groups <- as.list(rlang::enexpr(groups))[-1]
     dv <- enquo(dv)
       data %>% 
          group_by(!!! groups) %>% 
          summarise(group_mean = mean(!!dv))
     }
    
    mtcars %>% 
          foo(groups = c(vs, am), dv = mpg)
    # A tibble: 4 x 3
    # Groups:   vs [?]
    #     vs    am group_mean
    #  <dbl> <dbl>      <dbl>
    #1     0     0       15.0
    #2     0     1       19.8
    #3     1     0       20.7
    #4     1     1       28.4
    

    Or as @Joe mentioned in the comments, enquo should also work with group_by_at

    foo <- function(data, groups, dv){
       dv <- enquo(dv) 
       groups <- enquos(groups) 
       data %>% 
            group_by_at(vars(!!!groups)) %>% 
            summarise(group_mean = mean(!!dv))
       } 
    
    mtcars %>% 
         foo(groups = c(vs, am), dv = mpg)
    # A tibble: 4 x 3
    # Groups:   vs [?]
    #     vs    am group_mean
    #  <dbl> <dbl>      <dbl>
    #1     0     0       15.0
    #2     0     1       19.8
    #3     1     0       20.7
    #4     1     1       28.4