We recently introduced a new paragraph to the development version of our dev guide
Provide a way for users to opt out of verbosity, preferably at the package level: make message creation dependent on an environment variable or option (like “usethis.quiet” in the usethis package), rather than on a function parameter. The control of messages could be on several levels (“none, “inform”, “debug”) rather than logical (no messages at all / all messages). Control of verbosity is useful for end users but also in tests. More interesting comments can be found in an issue of the tidyverse design guide.
Nice read. I see a few exceptions to this otherwise very sensible guideline, which may be worth highlighting:
When we actually want to control the verbosity of specific functions rather than of the entire package. This is relevant when multiple functions form a pipeline and we want to give package user control over verbosity in specific parts of the pipeline, e.g. start pipeline with function run_pipeline, which has arguments verbose_compute_step1, verbose_compute_step2 and verbose_plot to control the inner verbosity.
Starting an R proces with a single command (function call) on a single line can be powerful as it does not require additional code comments or wrapper functions to clarify that the options() call and the function call act as one unit. I sometimes find it impractical when functions expect me to always run an options() call prior and another options() call to reset options afterward.
When we want to send progress updates of a long running computation to the console. For this purpose I like function level verbosity with cat() because cat() is displayed in a neutral color aligned with the users’ own IDE color configuration instead of the alarming red as used for message(). All of this is important to me because I want errors and warnings to stand out in color in between all the trivial progress updates. My understanding is that ANSI sequences cannot query or adapt to the local IDE color settings as in we need to know whether user has black text on white background or white text on black background to decide whether to message() in black or white. Further, my understanding is that cat() cannot be controlled with options() in the way as shown in the blog post (please correct me if I am wrong!) therefore I see no other way then to use cat() and control verbosity at function level for this purpose.
For point 1, I see how that would be useful, have you considered an enum: verbose_compute = c("step1","step3"), that way you’d be able to keep your user interface constant when the number of steps change. In that same vein, you could still do this with options I suppose, but that might be less user friendly? Very interesting!
For point 2, have you considered withr? Options — with_options • withr, that’ll save you from setting and resetting options. Like you I am also a bit reluctant to change global state where I can avoid it.
Thanks a lot, I didn’t know about these tricks. You have taken away my concerns! Point 1 is not critical for me, but an attempt to identify what could be critical for others.
Thanks both @vincentvanhees@PietrH!
I guess you could also have an argument whose default value is the option, bla <- function(..., .verbose = getOption("mypkg.verbose", FALSE)). I’m adding a dot to the argument name so it’s clear it’s not necessary, but I feel it still makes the function signature longer to read.
I wonder what @mpadge thinks about this.
I agree it makes the signature more difficult to read. In general I think options are less well adopted or understood by users than boolean or enumerated arguments. However, I hope that most users wouldn’t need to change default verbosity levels all that much anyway.
What do you think about httr2::req_verbose(): Show extra output when request is performed — req_verbose • httr2 ? I quite like the idea of piping on a function that controls verbosity, that way it’s out of the way when you don’t need it, but has fine grained control that is documented on a single page, and can be maintained single file.
Tangent:
I feel required and non required arguments are better communicated by the presence or absence of a default value, if an argument is required, it should never have a default value, and if it has a default value, it should never be required. Tidy design principles - Required args shouldn’t have defaults got me fully on board.
There actually is a tidyverse design principle about the dot prefix: Tidy design principles - Dot prefix, but I haven’t fully grasped that one yet.
Nice read and I agree with most of what is mentioned in the post and the discussions. I only want to point out one feature the base R functions stop(), error(), warning() or stopifnot() share we should all love them for. It is the only place in R providing proper multilingual support! Of course this is a feature, if you use are working in an English language environment, you are likely not even aware of.
Error and warning messages often appear in German on my console, and especially for base R packages the support is excellent. For example (on a German console) stopifnot() translate messages whereas the plug-in replacement assert_that does not (yet??). How is the multilingual support for the suggested changes?
I agree warnings and error messages are often cryptic at best and there is also need to be better control. I tried once to translate the messages for one of my packages, but ended up improving all my messages instead. Translating messages is a good start contributing to and improving packages. And I would argue that messages end up less cryptic and a lot more useful for many users, if translations are provided. And less reasons to shut them up. This will also align nicely with rOpenSci effort in translations of documentation as mentioned in some recent blog posts, which I really appreciate.
Thanks all for these insightful responses! A couple of thoughts from my side:
@KlausVigo your point is indeed extremely important, and (… fortunately …) the use of rlang functions entirely respects multi-lingual messaging. I’m not about other, derived packages like cli?
The use of options is essentially to ease workflows which rely on ps/withr. These (can) spawn sub-processes which inherit options by default but not necessarily environments. There are a book’s worth of interesting parallels with other languages and systems, but essentially resolve to R being designed such that options provide a safer and more controllable way to pass these kinds of things between instances than environments.
Function-level control is something we perhaps should at least have mentioned. Sorry that we didn’t, and thank you @vincentvanhees for also raising a very important point. I have no particular thoughts or insights there, but suspect also that req_verbose() would provide a useful analogy for a general recommendation. Any package could have a simple function to make calls verbose, because rlang::local_options() would be reset at the end of the chain.
Thanks to the rOpenSci team for the guidance on this!
How should verbosity control via local or global options be documented? A section inside the documentation of each function that is concerned (e.g. #' @section Verbosity control:)? In the vignette?