inst/README.markdown

argufy

Declarative Argument Checks

Project Status: WIP - Initial development is in progress, but there has not yet been a stable, usable release suitable for the public. Linux Build Status Windows Build status CRAN RStudio mirror downloads Coverage Status

Declare your functions with argument checks, and argufy generates and inserts the checking code for you.

Installation

devtools::install_github("gaborcsardi/argufy")

Usage

Introduction

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 the Imports entry of the DESCRIPTION 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:

r #' @importFrom argufy argufy_me NULL argufy::argufy_me()

Assertions

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.

Internal functions

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)
#>     }
#> }

Coercions

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.

More concise assertions with .

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 involving multiple arguments

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
#> }

Reuse assertions for multiple functions

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))
#>     }
#> }

Without Roxygen

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.

Frequently asked questions

I get an R CMD check NOTE

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.

I get warnings when building my package

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.

Assertions do not show up when using devtools

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.

How can I know that argufy worked during installation

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.

License

MIT © Gábor Csárdi, Jim Hester.



gaborcsardi/argufy documentation built on May 16, 2019, 4:07 p.m.