Search code examples
rdata.tablelazy-evaluation

Evaluating ellipsis (...) in data.table context


I am writing a function that pastes arguments and returns empty string if an argument is evaluated with error. It is easy for two arguments:

paste_clean_ab <- function(a,b){
  a <- tryCatch(a, error = function(cond) {warning(cond); return(NULL);})
  b <- tryCatch(b, error = function(cond) {warning(cond); return(NULL);})
  if (is.null(a) | is.null(b)) return('');
  return(paste0(a,b))
}

Works as expected:

paste_clean_ab('A', 'B')
# "AB"

var1='VAR1'
paste_clean_ab('A=', var1)
# "A=VAR1"

paste_clean_ab('A ', non_existing_variable)
# ""
# Warning message:
# In doTryCatch(return(expr), name, parentenv, handler) :
#   object 'non_existing_variable' not found

# vectorized
paste_clean_ab(letters[1:5], var1)
# "aVAR1" "bVAR1" "cVAR1" "dVAR1" "eVAR1"

Importantly, it also works in data.table:

require(data.table)
data(iris)
dt.iris <- data.table(iris)
dt.iris[,Label1:=paste_clean_ab('Species: ',as.character(Species))]
dt.iris[,Label2:=paste_clean_ab('Species: ',non_existing_variable)]
# Warning message:
# In doTryCatch(return(expr), name, parentenv, handler) :
#   object 'non_existing_variable' not found

Now, I want to extend this function to work with any number of arguments. Here is my solution for now:

paste_clean <- function(...){
  arglistS <- as.list(substitute(list(...)))
  ret <- ''

  for (arg in arglistS){
    argVal <- tryCatch(eval(arg), error = function(cond) {warning(cond); return(NULL);})
  
    # Skipping first element of the resulting list:
    if (isTRUE(attr(argVal, 'class')=='result') & class(arg)=='name') next; # R v 4.1
    if (identical(argVal, .Primitive('list'))) next; # R v 4.2+
  
    if (is.null(argVal)) return('')
    ret <- paste0(ret,argVal);
  }  
  
  return(ret)
}

It works fine with any ordinary variables:

paste_clean('A','B','C')
# "ABC"

var1='VAR1'
var2='VAR2'
paste_clean('A=',var1, '; B=',var2)
# "A=VAR1; B=VAR2"

paste_clean('A=',non_existing_variable,var2)
# ""
# Warning message:
# In eval(arg) : object 'non_existing_variable' not found

dt.iris[,Label3:=paste_clean('var1: ',var1)] # "var1: VAR1"

But I can't address any existing columns in the data.table:

dt.iris[,Label4:=paste_clean('Species: ', Species)]
# Warning message:
# In eval(arg) : object 'Species' not found

What should I change in my tryCatch() to make it evaluate in the right context?


Solution

  • You could specify evaluation environment : parent.frame().
    This seems to work with the examples you provided, but isn't fully bulletproof as I got an error instead of the console mode warning using reprex::reprex() to verify my answer.

    paste_clean <- function(...){
      arglistS <- as.list(substitute(list(...)))
      
      #arglistS <- list(...)
      ret <- ''
      
      for (arg in arglistS){
        argVal <- tryCatch(eval(arg,parent.frame()), error = function(cond) {warning(cond); return(NULL);})
        
        # Skipping first element of the resulting list:
        if (isTRUE(attr(argVal, 'class')=='result') & class(arg)=='name') next; # R v 4.1
        if (identical(argVal, .Primitive('list'))) next; # R v 4.2+
        
        if (is.null(argVal)) return('')
        ret <- paste0(ret,argVal);
      }  
      
      return(ret)
    }
    paste_clean('A','B','C')
    #> [1] "ABC"
    
    var1='VAR1'
    var2='VAR2'
    paste_clean('A=',var1, '; B=',var2)
    #> [1] "A=VAR1; B=VAR2"
    
    
    paste_clean('A=',non_existing_variable,var2)
    #> [1] ""
    # Warning message:
    # In eval(arg, parent.frame()) : object 'non_existing_variable' not found
    
    dt.iris[,Label4:=paste_clean('Species: ', Species)][,Label4]
    #>   [1] "Species: setosa"     "Species: setosa"     "Species: setosa"    
    #>   [4] "Species: setosa"     "Species: setosa"     "Species: setosa"    
    ...