Search code examples
rrlangr-glue

Get list element name from index


I'm trying to use the following glue code to create an informative error message

library(rlang)
library(glue)

my_function <- function(x) {
  UseMethod("my_function", x)
}

my_function.default <- function(x) {
  abort(glue(
    "Can't calculate my_function because { deparse(substitute(x)) } is of type ",
    glue_collapse(class(x))
  ))
}

Using this test list we see it works:

test <- list(
  x = c(1,2,3),
  y = c("one", "two", "three")
)

my_function(test[[1]])
Error: Can't calculate my_function because test[[1]] is of type numeric
Run `rlang::last_error()` to see where the error occurred. 

But is it possible to use glue to have the the error return x where it says test[[1]] resulting in the error:

Can't calculate my_function because x is of type numeric


Solution

  • Here's a function that digs into an indexing expression to infer the name of the element being indexed. Briefly, it converts expressions that follow the list[index] pattern to names(list)[index], while being smart about list$name already having the name in the expression.

    getElementNames <- function(ee) {
        ## Determine if ee is an indexing operation
        eel <- as.list(ee)
        isIdx <- purrr::map_lgl(exprs( `[`, `[[`, `$` ),
                                identical, eel[[1]])
    
        ## If not, return the expression itself as a string
        if(!any(isIdx)) return( deparse(ee) )
    
        ## The name may already be in the expression
        if( is.name(eel[[3]]) || is.character(eel[[3]]) )
            return( as.character(eel[[3]]) )
    
        ## Compose an expression indexing the names
        nms <- eval.parent(expr( names(!!eel[[2]])[!!eel[[3]]] ))
    
        ## Names might be missing
        `if`( is.null(nms), deparse(ee), nms )
    }
    

    The function in action:

    test  <- list(a=4, b=5, c=6)
    test2 <- 1:3
    ftest <- function(x) abort(glue("Can't calculate {getElementNames(substitute(x))}"))
    
    ftest( test[[2]] )    # index by numeric value
    # Error: Can't calculate b
    
    ftest( test$c )       # index by name
    # Error: Can't calculate c
    
    ftest( test[["a"]] )  # another way to index by name
    # Error: Can't calculate a
    
    i <- 2; j <- 3
    ftest( test[i:j] )    # index multiple elements
    # Error: Can't calculate b
    # * Can't calculate c
    
    ftest( test2[3] )     # index something with no names
    # Error: Can't calculate test2[3]
    
    ftest( fun_that_returns_list() )  # non-indexing expression
    # Error: Can't calculate fun_that_returns_list()
    
    ftest( 1:3 )                      # another non-indexing expression
    # Error: Can't calculate 1:3