Declarative Argument Checks
Declare your functions with argument checks, and argufy
generates
and inserts the checking code for you.
devtools::install_github("gaborcsardi/argufy")
To use argufy
in your R package, you need to import and call the
argufy_me
function. Once you called argufy_me
, you can add
assertions and coercions to your Roxygen headers, and these will be picked
up automatically at installation time. You also need to add argufy
to the RdMacros
entry of the package.
In other words, importing argufy_me
requires two qiuck and easy
steps:
-
Include
argufy
in theImports
entry of theDESCRIPTION
file:... Imports: argufy RdMacros: argufy ...
-
Import the
argufy_me
function in a Roxygen header, and call the function. I.e. put the following in any of your.R
source files:#' @importFrom argufy argufy_me NULL argufy::argufy_me()
State your assertions in the @param
tags of your Roxygen
documentation, inside \assert{}
Rd tags. An assertion is
an R expression. It must evaluate to TRUE
, each time the
function is called, and the argument is supplied, otherwise
the function quits with an error.
The assertions are parsed by Roxygen, and put in the manual page of the function as well. They are also parsed when your package is installed, and they are inserted to the body of the function(s) they refer to, automatically.
When the user of your package loads your package via library()
or imports it into another package, the assertions are checked
and an error is given if any of them fail.
Let's see an example.
#' Prefix of a string
#'
#' @param str \assert{is.character(str)} Character vector.
#' @param len \assert{is.integer(len)} Integer vector.
#' @return Prefix is string, of prescribed length.
prefix <- function(str, len) {
substring(str, 1, len)
}
If you call Roxygen to create the Rd documentation for this function,
and then install the package, argufy
generates code from your assertions
and injects the code into the body of the function:
#> function (str, len)
#> {
#> {
#> stopifnot(is.character(str))
#> stopifnot(is.integer(len))
#> }
#> {
#> substring(str, 1, len)
#> }
#> }
The assertions are also included in the generated manual pages:
Arguments:
str: [‘is.character(str)’] Character vector.
len: [‘is.integer(len)’] Integer vector.
It is possible to add assertions to internal functions, with a little bit
of extra work. If an internal function has assertions defined in @param
Roxygen tags, it is also required to add the @keywords internal
tag, and
a manual page title. Roxygen will generate a manual page for the function
in this case, but will not include it in the index of manual pages:
#' This is an internal function that merges two named lists, elementwise,
#' @param x \assert{is_named_list} First list.
#' @param y \assert(is_named_list} Second list.
#' @keywords internal
merge_lists <- function(x, y) {
names <- unique(sort(c(names(x), names(y))))
structure(
lapply(names, function(n) { c(x[[n]], y[[n]]) }),
names = names
)
}
The generated code:
#> function (x, y)
#> {
#> {
#> stopifnot(is_named_list(x))
#> stopifnot(is_named_list(y))
#> }
#> {
#> names <- unique(sort(c(names(x), names(y))))
#> structure(lapply(names, function(n) {
#> c(x[[n]], y[[n]])
#> }), names = names)
#> }
#> }
Quite often, coercing the argument to the desired type is a better
solution than a simple assertion, because it makes your function
extensible. E.g. if your function takes a data frame argument, then
instead of checking that the supplied object is indeed a data frame,
you can try to coerce it to a data frame. This way, your function will
work for any object that is coercible to a data frame (i.e. has an
as.data.frame()
method).
argufy
has a \coerce{}
tag to declare coercions. It works very
similarly to the \assert{}
tag, but the generated code is different:
#' Prefix of a string
#'
#' @param str \coerce{as.character(str)} Character vector.
#' @param len \coerce{as.integer(len)} Integer vector.
#' @return Prefix is string, of prescribed length.
prefix2 <- function(str, len) {
substring(str, 1, len)
}
And the generated code:
#> function (str, len)
#> {
#> {
#> str <- as.character(str)
#> len <- as.integer(len)
#> }
#> {
#> substring(str, 1, len)
#> }
#> }
The coercion expression must fail by calling stop()
if it is not
possible to coerce the supplied value in a meaningful way.
You can of course mix assertions and coercions for the same function.
argufy
helps writing short and concise assertions. If your assertion
is a single function call on the supplied argument, you can simply
use the name of the function instead. For example:
#' Prefix of a string
#'
#' @param str \assert{is.character} Character vector.
#' @param len \assert{is.integer} Integer vector.
#' @return Prefix is string, of prescribed length.
prefix <- function(str, len) {
substring(str, 1, len)
}
If your assertion is more complex, then you can use a dot: .
instead
of the argument name:
#' Prefix of a string
#'
#' @param str \assert{is.character} Character vector.
#' @param len \assert{is.integer(.) && length(.) == 1} Integer vector.
#' @return Prefix is string, of prescribed length.
prefix <- function(str, len) {
substring(str, 1, len)
}
Assertions can refer to multiple arguments by name. An assertion can refer to any other argument, the order of the arguments does not matter at all:
#' Sum of two matrices
#'
#' @param A \assert{is.matrix(.) && identical(dim(A), dim(B))}
#' The first matrix.
#' @param B \assert{is.matrix(.) && identical(dim(A), dim(B))}
#' The second matrix.
#' @return Their sum.
plusmat <- function(A, B) A + B
The generated code:
#> function (A, B)
#> {
#> {
#> stopifnot(is.matrix(A) && identical(dim(A), dim(B)))
#> stopifnot(is.matrix(B) && identical(dim(A), dim(B)))
#> }
#> A + B
#> }
If you declare an assertion for an argument, it will be used for all functions that share that argument, and are documented on that same Rd manual page. For example:
#' Prefix of a string
#'
#' @param str \assert{is.character} Character vector.
#' @param len \assert{is.integer} Integer vector.
#' @return Prefix is string, of prescribed length.
prefix <- function(str, len) {
substring(str, 1, len)
}
#' Suffix of a string
#'
#' @rdname prefix
suffix <- function(str, len) {
substring(str, nchar(str) - len + 1, nchar(str))
}
The generated code for suffix:
#> function (str, len)
#> {
#> {
#> stopifnot(is.character(str))
#> stopifnot(is.character(len))
#> }
#> {
#> substring(str, nchar(str) - len + 1, nchar(str))
#> }
#> }
You can also use argufy
without Roxygen. Simply put your assertions and
coercions in the Rd manual pages, using the \assert
and \coerce
macros.
They are automatically added to the functions at install time.
Right now R CMD check
gives the following note about the RdMacros
field in DESCRIPTION
:
Unknown, possibly mis-spelled, field in DESCRIPTION:
‘RdMacros’
This is a bug in R CMD check
. RdMacros
is a valid entry, it is
documented in Writing R extensions.
Warning: /tmp/Rtmp4dc3Q5/Rbuild5b3c7afe189/mypackage/man/myfun.Rd:10:
unknown macro '\assert'
This is probably because you did not add the RdMacros: argufy
entry
to DESCRIPTION
.
This is because the srcref
attribute of the function is not modified,
so when printing it to the screen, the original, unpatched code is shown.
Use print(func, useSource = FALSE)
to see the real, patched source code.
Various ways:
-
You'll see a message during installation:
** preparing package for lazy loading ** argufying functions ** help
-
Check the source code of the functions in the installed packages.
-
Check the manual pages of the installed package.
-
Try calling the functions with assertions and see if you get the expected error message(s).
-
You can also write test cases for it.
MIT © Gábor Csárdi, Jim Hester.