purrr
purrr copied to clipboard
Feature Request: Extend safely to encompass quietly as well
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).
TRUEwould be the same as all 4 andFALSEwould be the same ascharacter(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>
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.
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>
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?
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.