purrr icon indicating copy to clipboard operation
purrr copied to clipboard

Feature Request: Extend safely to encompass quietly as well

Open billdenney opened this issue 7 years ago • 3 comments
trafficstars

Currently, safely returns a list with the outputs for "results" and "error" with only one output. In many scenarios when looking to collect info like safely and quietly do, I'd prefer to have all of the outputs, warnings, and errors captured.

I think that ideally, the quiet option for safely would be extended to choose what is captured:

  • c("error", "warnings", "messages", "output") hide the outputs listed (and the others would be displayed). TRUE would be the same as all 4 and FALSE would be the same as character(0).
  • TRUE (hide and capture everything)
  • FALSE (show everything; still capture everything)

safely would then return a list with elements for result, output, messages, warnings, and error. An example of a function capturing everything is safely_n_quietly below:

library(purrr)

safely_n_quietly <- function(.f, otherwise = NULL) {
  retfun <- quietly(safely(.f, otherwise = otherwise, quiet = FALSE))
  function(...) {
    ret <- retfun(...)
    list(result = ret$result$result,
         output = ret$output,
         messages = ret$messages,
         warnings = ret$warnings,
         error = ret$result$error)
  }
}

silly_fun_1 <- function() {
  cat("hello cat")
  message("hello msg")
  warning("hello warn")
  # stop('hello stop')
  "A"
}

safely_n_quietly(silly_fun_1)()
#> $result
#> [1] "A"
#> 
#> $output
#> [1] "hello cat"
#> 
#> $messages
#> [1] "hello msg\n"
#> 
#> $warnings
#> [1] "hello warn"
#> 
#> $error
#> NULL

silly_fun_2 <- function() {
  cat("hello cat")
  message("hello msg")
  warning("hello warn")
  stop("hello stop")
  "A"
}

safely_n_quietly(silly_fun_2)()
#> $result
#> NULL
#> 
#> $output
#> [1] "hello cat"
#> 
#> $messages
#> [1] "hello msg\n"         "Error: hello stop\n"
#> 
#> $warnings
#> [1] "hello warn"
#> 
#> $error
#> <simpleError in .f(...): hello stop>

billdenney avatar Dec 15 '17 18:12 billdenney

I suggest to add an argument to quietly instead and make it work like quietly(stop)("a") stops quietly(stop, NULL)("a") returns

list(
    result = NULL, 
    output = "", 
    messages = character(0), 
    warnings = character(0), 
    error = simpleError("a") # With appropriate context
)

and quietly(cat, NULL)("a") returns

list(
    result = NULL,
    output = "a",
    messages = character(0),
    warnings = character(0),
    error = NULL
)

The only difference in a non-error result to quietly() is the presence of $error = NULL.

NB that the suggested change of behaviour in safely would break code relying on quietly = TRUE only suppressing errors.

AshesITR avatar Feb 10 '18 11:02 AshesITR

Using rlang, I made something that is pretty similar. To allow better control and understanding by the user, everything is returned as a list rather than flattening to just the messages associated with the error, warning, and message. Also, it runs the function directly rather than providing a new function (ergo the different name format with capture_everything() matching the internal functions used for a similar purpose).

It should also capture any generic condition signaled in addition to the most common types (messages, warnings, and errors).

stopper <- function() {
  print("bar")
  message("baz")
  warning("flop")
  rlang::cnd_signal(rlang::cnd(.type = "my_cond", .msg = "my message"))
  stop("foo")
  "bar"
}

goer <- function() {
  print("bar")
  message("baz")
  warning("flop")
  rlang::cnd_signal(rlang::cnd(.type = "my_cond", .msg = "my message"))
  # stop('foo')
  "blop"
}

capture_everything <- function(.expr, otherwise = NULL) {
  logger <- function() {
    log <- list()
    function(x, values = FALSE) {
      if (values) {
        log
      } else {
        log <<- append(log, list(x))
        NULL
      }
    }
  }
  
  messages <- logger()
  warnings <- logger()
  errors <- logger()
  conditions <- logger()
  temp <- file()
  sink(temp)
  on.exit({
    sink()
    close(temp)
  })
  result <- rlang::with_handlers(.expr, message = rlang::inplace(messages, 
    muffle = TRUE), warning = rlang::inplace(warnings, muffle = TRUE), error = rlang::exiting(errors), 
    condition = rlang::inplace(conditions))
  output <- paste0(readLines(temp, warn = FALSE), collapse = "\n")
  ret <- list(result = result, output = output, error = errors(values = TRUE), 
    warnings = warnings(values = TRUE), messages = messages(values = TRUE), 
    conditions = conditions(values = TRUE))
  if (length(ret$error)) {
    ret$result <- otherwise
  }
  ret
}

bar <- capture_everything(goer())
bar
#> $result
#> [1] "blop"
#> 
#> $output
#> [1] "[1] \"bar\""
#> 
#> $error
#> list()
#> 
#> $warnings
#> $warnings[[1]]
#> <simpleWarning in goer(): flop>
#> 
#> 
#> $messages
#> $messages[[1]]
#> <simpleMessage in message("baz"): baz
#> >
#> 
#> 
#> $conditions
#> $conditions[[1]]
#> <mufflable: my message>
foo <- capture_everything(stopper())
foo
#> $output
#> [1] "[1] \"bar\""
#> 
#> $error
#> $error[[1]]
#> <simpleError in stopper(): foo>
#> 
#> 
#> $warnings
#> $warnings[[1]]
#> <simpleWarning in stopper(): flop>
#> 
#> 
#> $messages
#> $messages[[1]]
#> <simpleMessage in message("baz"): baz
#> >
#> 
#> 
#> $conditions
#> $conditions[[1]]
#> <mufflable: my message>

billdenney avatar Aug 22 '18 03:08 billdenney

This would be great! Currently I'm using this solution https://stackoverflow.com/questions/4948361/how-do-i-save-warnings-and-errors-as-output-from-a-function/4952908#4952908 from Martin Morgan a lot but so far have avoided packaging it up as (a) it's written by someone else and (b) it's just a single function. @lionel- apologies for nudging but do you think that #725 will land in purrr at somepoint?

TimTaylor avatar Nov 13 '20 14:11 TimTaylor

I think this has evolved to be out of scope for purrr — it seems like you're reaching for something very similar to evaluate, and this level fidelity is out of scope for purrr.

hadley avatar Aug 24 '22 09:08 hadley