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.
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:
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.
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:
Note thatassert(, combine = "or") andassert(, combine = "and") allow to control the logicalcombination of the specified checks, and that the former is thedefault.
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.
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.
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.
x is a flaglibrary(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## 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 ax is a numeric of length 1000with no missing nor NaN valuesx=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 ax is a character vector withno missing values nor empty stringsx=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 bx is a data frame with nomissing valuesN=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# 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 cx is an increasing sequence ofintegers with no missing valuesN=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 cTo 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## [1] "Must store characters"## [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).
The package registers two functions which can be used in otherpackages’ C/C++ code for argument checks.
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:
checkmate to your “Imports” and “LinkingTo”sections in your DESCRIPTION file."checkmate_stub.c", seebelow.<checkmate.h> ineach compilation unit where you want to use checkmate.File contents for (2):
For the sake of completeness, here thesessionInfo() forthe benchmark (but remember the note before onknitrpossibly biasing the results).
## 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