0

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

Zoe
  • 27,060
  • 21
  • 118
  • 148
MayaGans
  • 1,815
  • 9
  • 30
  • I want it to return the name of the list element so if I did my_function(test[[2]]) right now it would return "Can't calculate my_function because y is of type character" – MayaGans Apr 14 '20 at 19:33
  • Do you want the name of the list element – akrun Apr 14 '20 at 19:37
  • 1
    When you pass in `test[[1]]`, you do not pass in the names as well. You could write code to possibly handle that, but then you'd also have special code for cases like `my_function(test$x)` or `my_function(fun_that_returns_list())` or just `my_function(1:3)` -- what happens in those cases? If you always expect a list, then you might want to specify separate parameters for the list itself and the subset parameter. – MrFlick Apr 14 '20 at 19:38
  • Ah ha interesting, that was my question, how to access the name of the list element when you're already *inside* it I guess that's just not possible? – MayaGans Apr 14 '20 at 19:39
  • 1
    it is not possible if it is inside. you can either pass two parameters, one the list and the name and then it would be easier – akrun Apr 14 '20 at 19:40
  • 1
    It's not possible with any sort of normal evaluation in R. Values are not aware of the fact that they are in lists. It's only the case that lists know their values. – MrFlick Apr 14 '20 at 19:40

1 Answers1

1

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

Artem Sokolov
  • 13,196
  • 4
  • 43
  • 74