Search code examples
rfunctionfrequencyellipsislexical-scope

R: Lexical Scoping issue when creating a function with ellipsis argument


I make a lot of frequency tables in R and was working on writing my own quick frequency table (qft) function when I ran into what I believe is a lexical scoping issue. Here is the version of my function that does not work

library("tidyverse")

data(mtcars)

qft_bad<-function(data,...){
  ft<-data.frame(with(data, table(...)))
  ft<-ft[ft$Freq!=0,]
  return(ft)
}

tst_bad<-qft_bad(mtcars,cyl,gear)

You will notice that if you try to run qft_bad() that the Error "Error in table(...) : object 'cyl' not found" is produced. As a work around I wrote the following function which does produce the desired result but requires slightly different inputs.

qft_good<-function(data,...){
      nmes<-c(...)
      vars<-dplyr::select(data,...)
      ft<-data.frame(table(vars))
      ft<-ft[ft$Freq!=0,]
      colnames(ft)[1:length(nmes)]<-nmes
      return(ft)
}

tst_good<-qft_good(mtcars,"cyl","gear")

I'm guessing qft_bad() does not work because R tries to evaluate the arguments outside of the input dataset but am a little unclear as to the specifics of this problem (is there some issue with the with() function?).

As qft_good() works well enough for my purposes I am mainly asking this question for my own R Enlightenment. Can anyone provide a better explanation what's going on in my qft_bad() function, or create a version of the qft function that does not require you to list the variable names in quotes (as you must in qft_good())?


Solution

  • You can use quosures from rlang to capture the arguments in ... then unquote splice them into a select call:

    library(tidyverse)
    library(rlang)
    
    qft <- function(data, ...){
      args <- enquos(...)
      vars <- select(data, !!!args)
      ft <- data.frame(table(vars))
      ft[ft$Freq != 0, ]
    }
    
    qft(mtcars, cyl, gear)
    #  cyl gear Freq
    #1   4    3    1
    #2   6    3    2
    #3   8    3   12
    #4   4    4    8
    #5   6    4    4
    #7   4    5    2
    #8   6    5    1
    #9   8    5    2