Movatterモバイル変換


[0]ホーム

URL:


Checkmate

Michel Lang

2025-08-18

Ever used an R function that produced a not-very-helpful errormessage, just to discover after minutes of debugging that you simplypassed a wrong argument?

Blaming the laziness of the package author for not doing suchstandard checks (in a dynamically typed language such as R) is at leastpartially unfair, as R makes these types of checks cumbersome andannoying. Well, that’s how it was in the past.

Enter checkmate.

Virtuallyevery standard type of user error whenpassing arguments into function can be caught with a simple, readableline which produces aninformative error message incase. A substantial part of the package was written in C tominimize any worries about execution time overhead.

Intro

As a motivational example, consider you have a function to calculatethe faculty of a natural number and the user may choose between usingeither the stirling approximation or R’sfactorial function(which internally uses the gamma function). Thus, you have twoarguments,n andmethod. Argumentn must obviously be a positive natural number andmethod must be either"stirling" or"factorial". Here is a version of all the hoops you need tojump through to ensure that these simple requirements are met:

fact<-function(n,method ="stirling") {if (length(n)!=1)stop("Argument 'n' must have length 1")if (!is.numeric(n))stop("Argument 'n' must be numeric")if (is.na(n))stop("Argument 'n' may not be NA")if (is.double(n)) {if (is.nan(n))stop("Argument 'n' may not be NaN")if (is.infinite(n))stop("Argument 'n' must be finite")if (abs(n-round(n,0))>sqrt(.Machine$double.eps))stop("Argument 'n' must be an integerish value")    n<-as.integer(n)  }if (n<0)stop("Argument 'n' must be >= 0")if (length(method)!=1)stop("Argument 'method' must have length 1")if (!is.character(method)||!method%in%c("stirling","factorial"))stop("Argument 'method' must be either 'stirling' or 'factorial'")if (method=="factorial")factorial(n)elsesqrt(2* pi* n)* (n/exp(1))^n}

And for comparison, here is the same function using checkmate:

fact<-function(n,method ="stirling") {assertCount(n)assertChoice(method,c("stirling","factorial"))if (method=="factorial")factorial(n)elsesqrt(2* pi* n)* (n/exp(1))^n}

Function overview

The functions can be split into four functional groups, indicated bytheir prefix.

If prefixed withassert, an error is thrown if thecorresponding check fails. Otherwise, the checked object is returnedinvisibly. There are many different coding styles out there in the wild,but most R programmers stick to eithercamelBack orunderscore_case. Therefore,checkmate offersall functions in both flavors:assert_count is just analias forassertCount but allows you to retain yourfavorite style.

The family of functions prefixed withtest always returnthe check result as logical value. Again, you can usetest_count andtestCount interchangeably.

Functions starting withcheck return the error messageas a string (orTRUE otherwise) and can be used if you needmore control and, e.g., want to grep on the returned error message.

expect is the last family of functions and is intendedto be used with thetestthat package.All performed checks are logged into thetestthat reporter.Becausetestthat uses theunderscore_case, theextension functions only come in the underscore style.

All functions are categorized into objects to check on thepackagehelp page.

In case you miss flexibility

You can useassert toperform multiple checks at once and throw an assertion if all checksfail.

Here is an example where we check that x is either of classfoo or classbar:

f<-function(x) {assert(checkClass(x,"foo"),checkClass(x,"bar")  )}

Note thatassert(, combine = "or") andassert(, combine = "and") allow to control the logicalcombination of the specified checks, and that the former is thedefault.

Argument Checks for the Lazy

The following functions allow a special syntax to define argumentchecks using a special format specification. E.g.,qassert(x, "I+") asserts thatx is an integervector with at least one element and no missing values. This very simpledomain specific language covers a large variety of frequent argumentchecks with only a few keystrokes. You choose what you like best.

checkmate as testthat extension

To extendtestthat, youneed to IMPORT, DEPEND or SUGGEST on thecheckmate package.Here is a minimal example:

# file: tests/test-all.Rlibrary(testthat)library(checkmate)# for testthat extensionstest_check("mypkg")

Now you are all set and can use more than 30 new expectations in yourtests.

test_that("checkmate is a sweet extension for testthat", {  x=runif(100)expect_numeric(x,len =100,any.missing =FALSE,lower =0,upper =1)# or, equivalent, using the lazy style:qexpect(x,"N100[0,1]")})

Speed considerations

In comparison with tediously writing the checks yourself in R (c.f.factorial example at the beginning of the vignette), R is sometimes atad faster while performing checks on scalars. This seems odd at first,because checkmate is mostly written in C and should be comparably fast.Yet many of the functions in thebase package are notregular functions, but primitives. While primitives jump directly intothe C code, checkmate has to use the considerably slower.Call interface. As a result, it is possible to write (verysimple) checks using only the base functions which, under somecircumstances, slightly outperform checkmate. However, if you go onestep further and wrap the custom check into a function to convenientre-use it, the performance gain is often lost (see benchmark 1).

For larger objects the tide has turned because checkmate avoids manyunnecessary intermediate variables. Also note that the quick/lazyimplementation inqassert/qtest/qexpect is often atad faster because only two arguments have to be evaluated (the objectand the rule) to determine the set of checks to perform.

Below you find some (probably unrepresentative) benchmark. But alsonote that this one here has been executed from insideknitrwhich is often the cause for outliers in the measured execution time.Better run the benchmark yourself to get unbiased results.

Benchmark 1: Assert thatx is a flag

library(checkmate)library(ggplot2)library(microbenchmark)x=TRUEr=function(x,na.ok =FALSE) {stopifnot(is.logical(x),length(x)==1, na.ok||!is.na(x)) }cm=function(x)assertFlag(x)cmq=function(x)qassert(x,"B1")mb=microbenchmark(r(x),cm(x),cmq(x))
## Warning in microbenchmark(r(x), cm(x), cmq(x)): less accurate nanosecond times## to avoid potential integer overflows
print(mb)
## Unit: nanoseconds##    expr  min   lq     mean median   uq     max neval cld##    r(x) 1681 1763 13141.73   1845 1886 1117004   100   a##   cm(x) 1230 1271  7458.31   1312 1394  526563   100   a##  cmq(x)  738  820  4580.93    820  861  348459   100   a
autoplot(mb)

Benchmark 2: Assert thatx is a numeric of length 1000with no missing nor NaN values

x=runif(1000)r=function(x)stopifnot(is.numeric(x),length(x)==1000,all(!is.na(x)& x>=0& x<=1))cm=function(x)assertNumeric(x,len =1000,any.missing =FALSE,lower =0,upper =1)cmq=function(x)qassert(x,"N1000[0,1]")mb=microbenchmark(r(x),cm(x),cmq(x))print(mb)
## Unit: microseconds##    expr   min     lq     mean median     uq      max neval cld##    r(x) 9.061 9.6145 25.64140  9.963 10.455 1545.741   100   a##   cm(x) 2.952 3.0340  7.13974  3.157  3.239  356.946   100   a##  cmq(x) 2.911 3.0340  6.49522  3.075  3.157  334.437   100   a
autoplot(mb)

Benchmark 3: Assert thatx is a character vector withno missing values nor empty strings

x=sample(letters,10000,replace =TRUE)r=function(x)stopifnot(is.character(x),!any(is.na(x)),all(nchar(x)>0))cm=function(x)assertCharacter(x,any.missing =FALSE,min.chars =1)cmq=function(x)qassert(x,"S+[1,]")mb=microbenchmark(r(x),cm(x),cmq(x))print(mb)
## Unit: microseconds##    expr     min      lq      mean   median      uq      max neval cld##    r(x) 129.601 136.735 149.86730 138.3135 142.024 1174.896   100  a ##   cm(x)  53.710  53.956  60.25811  54.1610  54.489  525.333   100   b##  cmq(x)  58.999  61.828  65.18385  62.0125  62.402  387.122   100   b
autoplot(mb)

Benchmark 4: Test thatx is a data frame with nomissing values

N=10000x=data.frame(a =runif(N),b =sample(letters[1:5], N,replace =TRUE),c =sample(c(FALSE,TRUE), N,replace =TRUE))r=function(x)is.data.frame(x)&&!any(sapply(x,function(x)any(is.na(x))))cm=function(x)testDataFrame(x,any.missing =FALSE)cmq=function(x)qtest(x,"D")mb=microbenchmark(r(x),cm(x),cmq(x))print(mb)
## Unit: microseconds##    expr    min      lq     mean  median      uq      max neval cld##    r(x) 53.177 58.5685 74.46502 61.3155 62.9350 1251.853   100  a ##   cm(x) 22.550 22.8370 29.08622 23.0010 24.2925  467.318   100   b##  cmq(x) 18.860 19.0035 24.93005 19.3315 20.2540  503.111   100   b
autoplot(mb)

# checkmate tries to stop as early as possiblex$a[1]=NAmb=microbenchmark(r(x),cm(x),cmq(x))print(mb)
## Unit: nanoseconds##    expr   min      lq     mean  median      uq   max neval cld##    r(x) 40877 51434.5 53962.56 53566.5 55985.5 86264   100 a  ##   cm(x)  2870  3116.0  3601.03  3280.0  3505.5 12710   100  b ##  cmq(x)   451   492.0   657.23   574.0   697.0  6437   100   c
autoplot(mb)

Benchmark 5: Assert thatx is an increasing sequence ofintegers with no missing values

N=10000x.altrep=seq_len(N)# this is an ALTREP in R version >= 3.5.0x.sexp=c(x.altrep)# this is a regular SEXP OTOHr=function(x)stopifnot(is.integer(x),!any(is.na(x)),!is.unsorted(x))cm=function(x)assertInteger(x,any.missing =FALSE,sorted =TRUE)mb=microbenchmark(r(x.sexp),cm(x.sexp),r(x.altrep),cm(x.altrep))print(mb)
## Unit: microseconds##          expr    min     lq     mean  median      uq      max neval cld##     r(x.sexp) 23.247 25.420 25.85009 25.6250 25.8095   37.269   100 ab ##    cm(x.sexp)  9.553  9.635 13.87317  9.7990  9.9220  356.618   100 a c##   r(x.altrep) 23.657 25.789 37.31205 26.0145 26.3220 1137.627   100  b ##  cm(x.altrep)  1.886  1.968  2.12954  2.1115  2.2140    2.665   100   c
autoplot(mb)

Extending checkmate

To extend checkmate a customcheck* function has to bewritten. For example, to check for a square matrix one can re-use partsof checkmate and extend the check with additional functionality:

checkSquareMatrix=function(x,mode =NULL) {# check functions must return TRUE on success# and a custom error message otherwise  res=checkMatrix(x,mode = mode)if (!isTRUE(res))return(res)if (nrow(x)!=ncol(x))return("Must be square")return(TRUE)}# a quick test:X=matrix(1:9,nrow =3)checkSquareMatrix(X)
## [1] TRUE
checkSquareMatrix(X,mode ="character")
## [1] "Must store characters"
checkSquareMatrix(X[1:2, ])
## [1] "Must be square"

The respective counterparts to thecheck-function can becreated using the constructorsmakeAssertionFunction,makeTestFunctionandmakeExpectationFunction:

# For assertions:assert_square_matrix= assertSquareMatrix=makeAssertionFunction(checkSquareMatrix)print(assertSquareMatrix)
## function (x, mode = NULL, .var.name = checkmate::vname(x), add = NULL) ## {##     if (missing(x)) ##         stop(sprintf("argument \"%s\" is missing, with no default", ##             .var.name))##     res = checkSquareMatrix(x, mode)##     checkmate::makeAssertion(x, res, .var.name, add)## }
# For tests:test_square_matrix= testSquareMatrix=makeTestFunction(checkSquareMatrix)print(testSquareMatrix)
## function (x, mode = NULL) ## {##     isTRUE(checkSquareMatrix(x, mode))## }
# For expectations:expect_square_matrix=makeExpectationFunction(checkSquareMatrix)print(expect_square_matrix)
## function (x, mode = NULL, info = NULL, label = vname(x)) ## {##     if (missing(x)) ##         stop(sprintf("Argument '%s' is missing", label))##     res = checkSquareMatrix(x, mode)##     makeExpectation(x, res, info, label)## }

Note that all the additional arguments.var.name,add,info andlabel areautomatically joined with the function arguments of your custom checkfunction. Also note that if you define these functions inside an Rpackage, the constructors are called at build-time (thus, there is nonegative impact on the runtime).

Calling checkmate from C/C++

The package registers two functions which can be used in otherpackages’ C/C++ code for argument checks.

SEXPqassert(SEXP x, const char*rule, const char*name);Rbooleanqtest(SEXP x, const char*rule);

These are the counterparts toqassertandqtest. Dueto their simplistic interface, they perfectly suit the requirements ofmost type checks in C/C++.

For detailed background information on the register mechanism, seetheExporting C Codesection in Hadley’s Book “R Packages” orWRE.Here is a step-by-step guide to get you started:

  1. Addcheckmate to your “Imports” and “LinkingTo”sections in your DESCRIPTION file.
  2. Create a stub C source file"checkmate_stub.c", seebelow.
  3. Include the provided header file<checkmate.h> ineach compilation unit where you want to use checkmate.

File contents for (2):

#include <checkmate.h>#include <checkmate_stub.c>

Session Info

For the sake of completeness, here thesessionInfo() forthe benchmark (but remember the note before onknitrpossibly biasing the results).

sessionInfo()
## R version 4.5.1 (2025-06-13)## Platform: aarch64-apple-darwin20## Running under: macOS Sequoia 15.6## ## Matrix products: default## BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib ## LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1## ## locale:## [1] C/de_DE.UTF-8/de_DE.UTF-8/C/de_DE.UTF-8/de_DE.UTF-8## ## time zone: Europe/Berlin## tzcode source: internal## ## attached base packages:## [1] stats     graphics  grDevices utils     datasets  methods   base     ## ## other attached packages:## [1] microbenchmark_1.5.0 ggplot2_3.5.2        checkmate_2.3.3     ## ## loaded via a namespace (and not attached):##  [1] Matrix_1.7-0       gtable_0.3.6       jsonlite_2.0.0     dplyr_1.1.4       ##  [5] compiler_4.5.1     tidyselect_1.2.1   jquerylib_0.1.4    splines_4.5.1     ##  [9] scales_1.4.0       yaml_2.3.10        fastmap_1.2.0      TH.data_1.1-3     ## [13] lattice_0.22-7     R6_2.6.1           generics_0.1.4     knitr_1.50        ## [17] MASS_7.3-59        backports_1.5.0    tibble_3.3.0       bslib_0.9.0       ## [21] pillar_1.11.0      RColorBrewer_1.1-3 rlang_1.1.6        multcomp_1.4-28   ## [25] cachem_1.1.0       xfun_0.52          sass_0.4.10        cli_3.6.5         ## [29] withr_3.0.2        magrittr_2.0.3     digest_0.6.37      grid_4.5.1        ## [33] mvtnorm_1.3-3      sandwich_3.1-1     lifecycle_1.0.4    vctrs_0.6.5       ## [37] evaluate_1.0.4     glue_1.8.0         farver_2.1.2       codetools_0.2-20  ## [41] zoo_1.8-14         survival_3.8-3     rmarkdown_2.29     tools_4.5.1       ## [45] pkgconfig_2.0.3    htmltools_0.5.8.1

[8]ページ先頭

©2009-2025 Movatter.jp