Rasul89
Rasul89

Reputation: 598

r- Storing the warning messages without discarding results when using purrr::map

I have a list of inputs over which I loop using purrr::map. Using a tweaked version of purrr::possibly (for more details see here) Applying this version of "possibly" to my main function, I'm able to use purrr::map to loop over my input list and store the results, or, alternatively, the complete error or warning messages if the function (partially) fails.

While this works fine for inputs where the function works correctly and inputs where it fails so that an error is produced, it is less optimal when a warning is produced. The resulting output from purrr::map will store only the warning, but not the result.

input <- list(10, -1 , "A")

possibly2 <- function (.f) {
  .f <- purrr::as_mapper(.f)
  function(...) {
    tryCatch(.f(...), 
             error = function(e) {
               return(paste0("Error: ", e$message))
             }, warning = function(w) {
               return(paste0("Warning: ", w$message))
               }
    )
  }
}

safer_log <- possibly2(.f = log)

map(input, safer_log)  %>% 
set_names(c("1. good input", "2. warning", "3. error")) # just added for naming the list elements

This results in:

$`1. good input`
[1] 2.302585

$`2. warning`
[1] "Warning: NaNs produced"

$`3. error`
[1] "Error: non-numeric argument to mathematical function"

However, for the warnings, I would actually like to include the results as well. Similar to:

> log(-1)
[1] NaN
Warning message:
In log(-1) : NaNs produced

But I'm not really sure where the result of the main function is stored and how I could adapt the code to return it together with the warning. Probably, in cases where this happens, it is also necessary to split the output into elements $result $warning.

Upvotes: 2

Views: 424

Answers (1)

Mikko Marttila
Mikko Marttila

Reputation: 11898

You need to use a custom condition handler to capture warnings while retaining the result. That’s because tryCatch() unwinds the call stack, discarding the context from the lower level that signaled the condition. Only the condition object (the warning) is available at that point.

withCallingHandlers() is the function to use to install a custom handler. In order to prevent default behaviour, handlers need to invoke a restart. This can be done with tryInvokeRestart(). warning() makes a muffleWarning restart available to avoid printing the warning message. Here I also demonstrate adding your own restart using withRestarts(). Our custom error handler uses that restart to avoid aborting when an error is signaled, returning NULL instead. But you could just as well keep the tryCatch() for errors.

Here’s the modified function:

catching <- function(f) {
  function(...) {
    error <- NULL
    handleError <- function(e) {
      error <<- conditionMessage(e)
      tryInvokeRestart("useValue", NULL)
    }

    warnings <- list()
    handleWarning <- function(w) {
      msg <- conditionMessage(w)
      warnings <<- append(warnings, msg)
      tryInvokeRestart("muffleWarning")
    }

    result <- withCallingHandlers(
      error = handleError,
      warning = handleWarning,
      withRestarts(f(...), useValue = identity)
    )

    list(result = result, error = error, warnings = warnings)
  }
}

And some results:

lapply(list(1, -1, "a"), catching(log)) |> str()
#> List of 3
#>  $ :List of 3
#>   ..$ result  : num 0
#>   ..$ error   : NULL
#>   ..$ warnings: list()
#>  $ :List of 3
#>   ..$ result  : num NaN
#>   ..$ error   : NULL
#>   ..$ warnings:List of 1
#>   .. ..$ : chr "NaNs produced"
#>  $ :List of 3
#>   ..$ result  : NULL
#>   ..$ error   : chr "non-numeric argument to mathematical function"
#>   ..$ warnings: list()

For more about R’s condition system, I found this article particularly helpful: http://adv-r.had.co.nz/beyond-exception-handling.html.

Upvotes: 3

Related Questions