Search code examples
rdata.tablense

How to use non-standard evaluation NSE to evaluate arguments on data.table?


Say I have the following

library(data.table)
cars1 = setDT(copy(cars))
cars2 = setDT(copy(cars))

car_list = list(cars1, cars2)
class(car_list) <- "dd"

`[.dd` <- function(x,...) {
  code = rlang::enquos(...)
  cars1 = x[[1]]
  rlang::eval_tidy(quo(cars1[!!!code]))
}

car_list[,.N, by = speed]

so I wished to perform arbitrary operations on cars1 and cars2 by defining the [.dd function so that whatever I put into ... get executed by cars1 and cars2 using the [ data.table syntax e.g.

car_list[,.N, by = speed] should perform the following

cars1[,.N, by = speed]
cars2[,.N, by = speed]

also I want

car_list[,speed*2]

to do

cars1[,speed*2]
cars2[,speed*2]

Basically, ... in [.dd has to accept arbitrary code.

somehow I need to capture the ... so I tried to do code = rlang::enquos(...) and then rlang::eval_tidy(quo(cars1[!!!code])) doesn't work and gives error

Error in [.data.table(cars1, ~, ~.N, by = ~speed) : argument "i" is missing, with no default


Solution

  • First base R option is substitute(...()) followed by do.call:

    library(data.table)
    cars1 = setDT(copy(cars))
    cars2 = setDT(copy(cars))
    cars2[, speed := sort(speed, decreasing = TRUE)]
    
    car_list = list(cars1, cars2)
    class(car_list) <- "dd"
    
    `[.dd` <- function(x,...) {
      a <- substitute(...()) #this is an alist
      expr <- quote(x[[i]])
      expr <- c(expr, a)
      res <- list()
      for (i in seq_along(x)) {
        res[[i]] <- do.call(data.table:::`[.data.table`, expr)
      }
      res
    }
    
    all.equal(
      car_list[,.N, by = speed],
      list(cars1[,.N, by = speed], cars2[,.N, by = speed])
    )
    #[1] TRUE
    
    all.equal(
      car_list[, speed*2],
      list(cars1[, speed*2], cars2[, speed*2])
    )
    #[1] TRUE
    

    Second base R option is match.call, modify the call and then evaluate (you find this approach in lm):

    `[.dd` <- function(x,...) {
      thecall <- match.call()
      thecall[[1]] <- quote(`[`)
      thecall[[2]] <- quote(x[[i]])
      res <- list()
      for (i in seq_along(x)) {
        res[[i]] <- eval(thecall)
      }
      res
    }
    
    all.equal(
      car_list[,.N, by = speed],
      list(cars1[,.N, by = speed], cars2[,.N, by = speed])
    )
    #[1] TRUE
    
    all.equal(
      car_list[, speed*2],
      list(cars1[, speed*2], cars2[, speed*2])
    )
    #[1] TRUE
    

    I haven't tested if these approaches will make a deep copy if you use :=.