Skip to contents

The goal of cnd is to provide easy, customized classes for your conditions.

This makes setting up custom conditions quick and more useful.

Installation

You can install the current CRAN version of cnd with one of:

install.packages("cnd")
pak::pak("cran/cnd@*release")
pak::pak("jmbarbone/cnd@*release")

You can install the development version of cnd from GitHub with:

pak::pak("jmbarbone/cnd")

But is recommended to specify a pre-release tag, if available.

Conditions, in general

conditions are special objects that R will use for both signaling and messaging, primarily within the context of stop(), warning(), and message().

format(stop)[7:10]
#> [1] "        message <- conditionMessage(cond)"               
#> [2] "        call <- conditionCall(cond)"                     
#> [3] "        .Internal(.signalCondition(cond, message, call))"
#> [4] "        .Internal(.dfltStop(message, call))"
format(warning)[9:14]
#> [1] "        message <- conditionMessage(cond)"                   
#> [2] "        call <- conditionCall(cond)"                         
#> [3] "        withRestarts({"                                      
#> [4] "            .Internal(.signalCondition(cond, message, call))"
#> [5] "            .Internal(.dfltWarn(message, call))"             
#> [6] "        }, muffleWarning = function() NULL)"
format(message)[13:19]
#> [1] "    defaultHandler <- function(c) {"                          
#> [2] "        cat(conditionMessage(c), file = stderr(), sep = \"\")"
#> [3] "    }"                                                        
#> [4] "    withRestarts({"                                           
#> [5] "        signalCondition(cond)"                                
#> [6] "        defaultHandler(cond)"                                 
#> [7] "    }, muffleMessage = function() NULL)"

All functions accept a condition object as their first argument, which contains both the classes that will be signaled and a message that will be sent to the stderr(). You’ll notice, too, that warning() and message() call withRestarts(), which contain signalCondition() call. By default, these three functions create condition objects with an extra class of "error", "warning", or "message", respectively. cnd inserts itself into these processes by allowing users to define new condition objects to be signaled and with greater control over messaging.

Example

The workhorse of cnd is condition(), which is a special function of class cnd::condition_progenitor, which returns other special functions of class cnd::condition_generator. The cnd::condition_generator objects return conditions.

library(cnd)
condition
#> cnd::condition_progenitor
#> 
#> generator
#>   $ class    : <symbol> 
#>   $ message  : NULL
#>   $ type     : <language> c("condition", "message", "warning", "error")
#>   $ package  : <language> get_package()
#>   $ exports  : NULL
#>   $ help     : NULL
#>   $ registry : <symbol> package
#>   $ register : <language> !is.null(registry)
#> 
#> condition(s)
#> cnd:as_character_cnd_error/error
#> cnd:condition_message_generator/error
#> cnd:condition_overwrite/warning
#> cnd:invalid_condition/error
#> cnd:invalid_condition_message/error
#> cnd:match_arg/error
#> cnd:no_package_exports/warning
#> 
#> For a list of conditions: `cnd::conditions()`

Note: condition is of mode “function” but does not retain “function” as a class. condition also has several conditions which can be signaled directly or indirectly.

Use condition() to create a generator, then use that generator within your functions:

# cnd::condition_generator
bad_value <- condition(
  "bad_value",
  message = "Value has to be better",
  type = "error"
)

bad_value
#> cnd::condition_generator
#> bad_value/error

# condition
bad_value()
#> bad_value/error
#> (bad_value/cnd::condition/error/condition)
#> Value has to be better

foo <- function(x) {
  if (x < 0) {
    stop(bad_value())
  }
  x
}


foo(-1)
#> Error in foo(): <bad_value>
#> Value has to be better

The resulting cnd::condition_generator object can also take parameters that are used in creating a custom message.

bad_value2 <- condition(
  "bad_value2",
  message = function(x) {
    sprintf("`x` must be `>=0`. A value of `%s` is no good", format(x))
  },
  type = "error"
)

# a 'generator' is also printed, with formals
bad_value2
#> cnd::condition_generator
#> bad_value2/error 
#> 
#> generator
#>   $ x : <symbol>

# pass a value to the args to generate the condition message
bad_value2(0)
#> bad_value2/error
#> (bad_value2/cnd::condition/error/condition)
#> `x` must be `>=0`. A value of `0` is no good
bad_value2(-1)
#> bad_value2/error
#> (bad_value2/cnd::condition/error/condition)
#> `x` must be `>=0`. A value of `-1` is no good

# note: this does not provide any tests, so you may produce non-nonsensical messages
bad_value2(10)
#> bad_value2/error
#> (bad_value2/cnd::condition/error/condition)
#> `x` must be `>=0`. A value of `10` is no good


# now when used in your function:
foo <- function(x) {
  if (x < 0) {
    stop(bad_value2(x))
  }
  x
}

foo(-1.2)
#> Error in foo(): <bad_value2>
#> `x` must be `>=0`. A value of `-1.2` is no good

Your package

There are three things you can do to get the most out of cnd within your package.

  • Creating a registry within your package
  • Assigning a "condition" attribute to your functions
  • Documenting your conditions

Registry

A registry is a new environment that will store all of your conditions. This environment must exist within your package, an cnd will be able to find this and use it to connect your conditions to your functions and to other outputs.

Simple add cnd_registry() to an R/ script in your package. If you are going to save an store conditions as objects (recommended) then you should ensure that the cnd_registry() call is made before any conditions are created.

NOTE cnd_registry() is designed to use assign() within your package environment. Please read the documentation to ensure the environment is not masked by other objects.

NOTE By default, condition(registry = ) will pick up on the registry object within your package when you create your conditions and functions are loaded. However, interactive use may not provide the same results. See the examples in cnd_create_registry() for an example of how to create a new registry and assign conditions to the registry.

Assigning conditions

condition() has an argument for exports, which you can set to any function which you want to relate with any specific conditions.

If you add any functions to the exports option, package must be set. By default, package should be set to your development package, so you don’t need to explicitly assign it every time.

In your package, add cnd_exports() to an R/ script. This should be executed after all your conditions and their functions are created. This will add a new "conditions" attribute to your functions as well as a new "cnd::conditioned_function" class. The new class specifically updates the print() method to show the conditions assigned to the function.

Documentation

When you’ve created your conditions and assigned them to your functions, you may also want to provide documentation. Or, rather, you should always provide documentation.

cnd_document() will create a new {package}-cnd-conditions.R file for all conditions you have assigned to your package. Simply run the command when developing to generate a file listing all conditions. You can also include this after your call to cnd_exports() to ensure that all conditions are documented. The file is written for roxygen2 to generate the Rd files for your package.

If you want to include other information directly within your roxygen comments, you can use the cnd_section() function to grab all the conditions from a single functions and print out roxygen-friendly section information:

cat(cnd_section(cnd))
#> 
#> Conditions are generated through the [`{cnd}`][cnd::cnd-package] package.
#> The following conditions are associated with this function:
#> 
#> \describe{
#>   
#>   \item{[`cnd:cond_cnd_class/error`][cnd-cnd-conditions]}{
#>     [cnd()] simple calls the appropriate function: [stop()], [warning()], or [message()] based on the `type` parameter from [cnd::condition()].
#>   }
#> 
#> }
#> 
#> For more conditions, see: [cnd-cnd-conditions]

Typically, you may want to use this as such:

#' @section Conditions:
#' `r cnd_section(my_function)`

Retrieval

You can retrieve any conditions that are created with conditions(). By default this will list all conditions loaded, but can be filtered by specific packages.

conditions("cnd", type = "warning")
#> [[1]]
#> cnd::condition_generator
#> cnd:cnd_document_conditions/warning 
#> 
#> exports
#>   cnd::cnd_document()
#> 
#> [[2]]
#> cnd::condition_generator
#> cnd:condition_overwrite/warning 
#> 
#> generator
#>   $ old : <symbol> 
#>   $ new : <symbol> 
#> 
#> exports
#>   cnd::condition()
#> 
#> [[3]]
#> cnd::condition_generator
#> cnd:conditions_dots/warning 
#> 
#> help
#> The `...` parameter in [conditions()] is meant for convenience.  Only a single argument is allowed.  Other parameters must be named  explicitly.  For example:  ```r # Instead of this conditions("class", "package") # "package" is ignored with a warning  # Do this conditions(class = "class", package = "package") ``` 
#> 
#> exports
#>   cnd::conditions()
#> 
#> [[4]]
#> cnd::condition_generator
#> cnd:no_package_exports/warning 
#> 
#> help
#> The `exports` parameter requires a `package` 
#> 
#> exports
#>   cnd::condition()

cnd()

cnd() is a special function which will use the appropriate signaling and handling function based on type of condition provided. When the condition’s type is "error" or "warning", cnd() passes these directly through stop() and warning(), respectively. Both of these functions have .Internal() calls (i.e., .dfltStop() and .dfltWarn()), which makes would make them difficult to replicate. However, message() does not, and thus an equivalent wrapper is internally used which also controls for formatting:

foo_call <- function() {
  condition("foo_condition", "two\nlines", type = "message")()
}

# provides a character(2) vector output:
conditionMessage(foo_call())
#> [1] "<foo_condition>\ntwo\nlines"

message() uses a handler which simply collapses the message vector into a single string. Because of this, the lines are not always neatly separated:

message(foo_call())
#> <foo_condition>
#> two
#> lines

By contrast, the handlers invoked in cnd() will recognize each element as a separate line for the output. Also, the default is to provide more information about the call, in a different format:

cnd(foo_call())
#> <foo_condition>
#> two
#> lines

To get the a simpler message, you can use the options() function to change the cnd.message.format option to "simple".

local({
  op <- options(cnd.message.format = "simple", cnd.call = FALSE)
  on.exit(options(op))
  cnd(foo_call())
})
#> <foo_condition>
#> two
#> lines

Currently message() and therefore cnd() send message conditions to the stderr(), thus usually giving them an colored text.

Another benefit in using cnd(condition) is being able to control for messages printed to the stdout(). Using cat() can sometimes create noise that you’d rather suppress. Because cnd() uses an internal handler for message and condition types, a condition is signaled with singalCondition(), which can then be caught with calling handlers, using a provided "muffleCondition" restart:

con <- condition("foo_condition", "Hello\nthere", type = "condition")
my_fun <- function() cnd(con())
my_fun() # note the classes inside (...)
#> foo_condition/condition
#> (foo_condition/cnd::condition/condition)
#> Hello
#> there

withCallingHandlers(
  my_fun(), 
  foo_condition = function(c) {
    tryInvokeRestart("muffleCondition")
  }
)