# check_arg is only used within functions
#
# I) Example for the main class "scalar"
#
test_scalar = function(xlog, xnum, xint, xnumlt, xdate){
# when forming the type: you can see that case, order and spaces don't matter
check_arg(xlog, "scalarLogical")
check_arg(xnum, "numeric scalar")
check_arg(xint, " scalar Integer GE{0} ")
check_arg(xnumlt, "numeric scalar lt{0.15}")
# Below it is critical that there's no space between scalar and the parenthesis
check_arg(xdate, "scalar(Date)")
invisible(NULL)
}
# Following is OK
test_scalar()
test_scalar(xlog = FALSE, xnum = 55, xint = 5, xnumlt = 0.11, xdate = Sys.Date())
#
# Now errors, all the following are wrong arguments, leading to errors
# Please note the details in the error messages.
# logical
try(test_scalar(xlog = NA))
try(test_scalar(xlog = 2))
try(test_scalar(xlog = sum))
try(test_scalar(xlog = faefeaf5))
try(test_scalar(xlog = c(TRUE, FALSE)))
try(test_scalar(xlog = c()))
# numeric
try(test_scalar(xnum = NA))
try(test_scalar(xnum = 1:5))
try(test_scalar(xnum = Sys.Date()))
# integer
try(test_scalar(xint = 5.5))
try(test_scalar(xint = -1))
# num < 0.15
try(test_scalar(xnumlt = 0.15))
try(test_scalar(xnumlt = 0.16))
try(test_scalar(xnumlt = Sys.Date()))
# Date
try(test_scalar(xdate = 0.15))
#
# II) Examples for the globals: NULL, MBT, eval, evalset
#
test_globals = function(xnum, xlog = TRUE, xint){
# Default setting with NULL is only available in check_set_arg
# MBT (must be there) throws an error if the user doesn't provide the argument
check_set_arg(xnum, "numeric vector NULL{1} MBT")
# NULL allows NULL values
check_arg(xlog, "logical scalar safe NULL")
check_arg(xint, "integer vector")
list(xnum = xnum, xlog = xlog)
}
# xnum is required because of MBT option
try(test_globals())
# NULL{expr} sets the value of xnum to expr if xnum = NULL
# Here NULL{1} sets xnum to 1
test_globals(xnum = NULL)
# NULL (not NULL{expr}) does not reassign: xlog remains NULL
test_globals(xnum = NULL, xlog = NULL)
# safe NULL: doesn't accept NULL from data.frame (DF) subselection
# ex: the variable 'log' does not exist in the iris DF
try(test_globals(5, xlog = iris$log))
# but xnum accepts it
test_globals(iris$log)
#
# eval and evalset
#
test_eval = function(x1, x2, data = list(), i = c()){
check_arg(x1, "eval numeric vector", .data = data)
# evalset is in check_set_arg
check_set_arg(x2, "evalset numeric vector", .data = data)
# We show the variables
if(1 %in% i){
cat("x1:\n")
print(as.character(try(x1, silent = TRUE)))
}
if(2 %in% i){
cat("x2:\n")
print(as.character(try(x2, silent = TRUE)))
}
}
# eval: evaluates the argument both in the environment and the data
test_eval(x1 = Sepal.Length, data = iris) # OK
# if we use a variable not in the environment nor in the data => error
try(test_eval(x1 = Sopal.Length, data = iris))
# but eval doesn't reassign back the value of the argument:
test_eval(x1 = Sepal.Length, data = iris, i = 1)
# evaset does the same as eval, but also reasssigns the value obtained:
test_eval(x2 = Sepal.Length, data = iris, i = 2)
#
# III) Match and charin
#
# match => does partial matching, only available in check_set_arg
# charin => no partial matching, exact values required, but in check_arg
#
# match
#
# Note the three different ways to provide the choices
#
# If the argument has no default, it is kept that way (see x2)
# If the argument is not provided by the user,
# it is left untouched (see x3)
test_match = function(x1 = c("bonjour", "Au revoir"), x2, x3 = "test"){
# 1) choices set thanks to the argument default (like in match.arg)
check_set_arg(x1, "strict match")
# 2) choices set with the argument .choices
check_set_arg(x2, "match", .choices = c("Sarah", "Santa", "Santa Fe", "SANTA"))
# 3) choices set with the parentheses
check_set_arg(x3, "multi match(Orange, Juice, Good)")
cat("x1:", x1, "\nx2:", tryCatch(x2, error = function(e) "[missing]"), "\nx3:", x3, "\n")
}
# Everything below is OK
test_match()
test_match(x1 = "Au", x2 = "sar", x3 = c("GOOD", "or"))
test_match(x2 = "Santa")
# Errors caught:
try(test_match(x1 = c("Au", "revoir")))
try(test_match(x1 = "au"))
try(test_match(x1 = sum))
try(test_match(x1 = list(a = 1:5)))
try(test_match(x2 = "san"))
try(test_match(x2 = "santa"))
# Same value as x3's default, but now provided by the user
try(test_match(x3 = "test"))
try(test_match(x3 = c("or", "ju", "bad")))
# You can check multiple arguments at once
# [see details for multiple arguments in Section X)]
# Note that now the choices must be set in the argument
# and they must have the same options (ie multi, strict)
test_match_multi = function(x1 = c("bonjour", "Au revoir"), x2 = c("Sarah", "Santa"),
x3 = c("Orange", "Juice", "Good")){
# multiple arguments at once
check_set_arg(x1, x2, x3, "match")
cat("x1:", x1, "\nx2:", x2, "\nx3:", x3, "\n")
}
test_match_multi()
#
# charin
#
# charin is similar to match but requires the user to provide the exact value
# only the multi option is available
test_charin = function(x1 = "bonjour", x2 = "Sarah"){
# 1) set the choices with .choices
check_arg(x1, "charin", .choices = c("bonjour", "au revoir"))
# 2) set the choices with the parentheses
check_arg(x2, "multi charin(Sarah, Santa, Santa Fe)")
cat("x1:", x1, "\nx2:", x2, "\n")
}
# Now we need the exact values
test_charin("au revoir", c("Santa", "Santa Fe"))
# Errors when partial matching tried
try(test_charin("au re"))
#
# IV) Vectors and marices, equalities, dimensions and lengths
#
# You can restrict the length of objects with len(a, b)
# - if len(a, b) length must be in between a and b
# - if len(a, ) length must be at least a
# - if len(, b) length must be at most b
# - if len(a) length must be equal to a
# You can also use the special keywords len(data) or len(value),
# but then the argument .data or .value must also be provided.
# (the related example comes later)
#
# You can restrict the number of rows/columns with nrow(a, b) and ncol(a, b)
#
# You can restrict a matrix to be square with the 'square' keyword
#
# You can restrict the values an element can take with GE/GT/LE/LT,
# respectively greater or equal/greater than/lower or equal/lower than
# The syntax is GE{expr}, with expr any expression
# Of course, it only works for numeric values
#
# By default NAs are tolerated in vector, matrix and data.frame.
# You can refuse NAs using the keyword: 'no na' or 'nona'
#
test_vmat = function(xvec, xmat, xvmat, xstmat, xnamed){
# vector of integers with values between 5 and exp(3)
check_arg(xvec, "integer Vector GE{5} LT{exp(3)}")
# logical matrix with at least two rows and with 3 columns
check_arg(xmat, "logicalMatrix NROW(2,) NCOL(3)")
# vector or matrix (vmatrix) of integers or character strings
# with at most 3 observations
# NAs are not allowed
check_arg(xvmat, "vmatrix(character, integer) nrow(,3) no na")
# square matrix of integers, logicals reports errors
check_arg(xstmat, "strict integer square Matrix")
# A vector with names of length 2
check_arg(xnamed, "named Vector len(2)")
invisible(NULL)
}
# OK
test_vmat(xvec = 5:20, xmat = matrix(TRUE, 3, 3), xvmat = c("abc", 4, 3),
xstmat = matrix(1:4, 2, 2), xnamed = c(bon=1, jour=2))
# Vector checks:
try(test_vmat(xvec = 2))
try(test_vmat(xvec = 21))
try(test_vmat(xvec = 5.5))
# Matrix checks:
try(test_vmat(xmat = matrix(TRUE, 3, 4)))
try(test_vmat(xmat = matrix(2, 3, 3)))
try(test_vmat(xmat = matrix(FALSE, 1, 3)))
try(test_vmat(xmat = iris))
try(test_vmat(xvmat = iris))
try(test_vmat(xvmat = c(NA, 5)))
try(test_vmat(xstmat = matrix(1, 1, 3)))
try(test_vmat(xstmat = matrix(c(TRUE, FALSE, NA), 3, 3)))
# Named vector checks:
try(test_vmat(xnamed = 1:3))
try(test_vmat(xnamed = c(bon=1, jour=2, les=3)))
#
# Illustration of the keywords 'data', 'value'
#
# 'value'
# Matrix multiplication X * Y * Z
test_dynamic_restriction = function(x, y, z){
check_arg(x, "mbt numeric matrix")
check_arg(y, "mbt numeric matrix nrow(value)", .value = ncol(x))
check_arg(z, "mbt numeric matrix nrow(value)", .value = ncol(y))
# An alternative to the previous two lines:
# check_arg(z, "mbt numeric matrix")
# check_arg(y, "mbt numeric matrix nrow(value) ncol(value)",
# .value = list(nrow = ncol(x), ncol = nrow(z)))
x %*% y %*% z
}
x = matrix(1, 2, 3)
y = matrix(2, 3, 5)
z = matrix(rnorm(10), 5, 2)
test_dynamic_restriction(x, y, z)
# Now error
try(test_dynamic_restriction(x, matrix(5, 1, 2), z))
# 'data'
# Computing maximum difference between two matrices
test_dynamic_bis = function(x, y){
check_arg(x, "mbt numeric matrix")
# we require y to be of the same dimension as x
check_arg(y, "mbt numeric matrix nrow(data) ncol(data)", .data = x)
max(abs(x - y))
}
test_dynamic_bis(x, x)
# Now error
try(test_dynamic_bis(x, y))
#
# V) Functions and lists
#
# You can restrict the number of arguments of a
# function with arg(a, b) [see Section IV) for details]
test_funlist = function(xfun, xlist){
check_arg(xfun, "function arg(1,2)")
check_arg(xlist, "list len(,3)")
invisible(NULL)
}
# OK
test_funlist(xfun = sum, xlist = iris[c(1,2)])
# function checks:
try(test_funlist(xfun = function(x, y, z) x + y + z))
# list checks:
try(test_funlist(xlist = iris[1:4]))
try(test_funlist(xlist = list()))
#
# VI) Data.frame and custom class
#
test_df = function(xdf, xvdf, xcustom){
# data.frame with at least 100 observations
check_arg(xdf, "data.frame nrow(100,)")
# data.frame or vector (vdata.frame)
check_arg(xvdf, "vdata.frame")
# Either: i) object of class glm or lm
# ii) NA
# iii) NULL
check_arg(xcustom, "class(lm, glm)|NA|null")
invisible(NULL)
}
# OK
m = lm(Sepal.Length~Species, iris)
test_df(xdf = iris, xcustom = m)
test_df(xvdf = iris$Sepal.Length)
test_df(xcustom = NULL)
# data.frame checks:
try(test_df(xdf = iris[1:50,]))
try(test_df(xdf = iris[integer(0)]))
try(test_df(xdf = iris$Sepal.Length))
# Note that the following works:
test_df(xvdf = iris$Sepal.Length)
# Custom class checks:
try(test_df(xcustom = iris))
#
# VIII) Formulas
#
# The keyword is 'formula'
# You can restrict the formula to be:
# - one sided with 'os'
# - two sided with 'ts'
#
# You can restrict that the variables of a forumula must be in
# a data set or in the environment with var(data, env)
# - var(data) => variables must be in the data set
# - var(env) => variables must be in the environment
# - var(data, env) => variables must be in the data set or in the environment
# Of course, if var(data), you must provide a data set
#
# Checking multipart formulas is included. You can use left(a, b)
# and right(a, b) to put restrictions in the number of parts allowed
# in the left and right-hand-sides
#
test_formulas = function(fml1, fml2, fml3, fml4, data = iris){
# Regular formula, variables must be in the data set
check_arg(fml1, "formula var(data)", .data = data)
# One sided formula, variables in the environment
check_arg(fml2, "os formula var(env)")
# Two sided formula, variables in the data set or in the env.
check_arg(fml3, "ts formula var(data, env)", .data = data)
# One or two sided, at most two parts in the RHS, at most 1 in the LHS
check_arg(fml4, "formula left(,1) right(,2)")
invisible(NULL)
}
# We set x1 in the environment
x1 = 5
# Works
test_formulas(~Sepal.Length, ~x1, Sepal.Length~x1, a ~ b, data = iris)
# Now let's see errors
try(test_formulas(Sepal.Length~x1, data = iris))
try(test_formulas(fml2 = ~Sepal.Length, data = iris))
try(test_formulas(fml2 = Sepal.Length~x1, data = iris))
try(test_formulas(fml3 = ~x1, data = iris))
try(test_formulas(fml3 = x1~x555, data = iris))
try(test_formulas(fml4 = a ~ b | c | d))
try(test_formulas(fml4 = a | b ~ c | d))
#
# IX) Multiple types
#
# You can check multiple types using a pipe: '|'
# Note that global keywords (like NULL, eval, etc) need not be
# separated by pipes. They can be anywhere, the following are identical:
# - "character scalar | data.frame NULL"
# - "NULL character scalar | data.frame"
# - "character scalar NULL | data.frame"
# - "character scalar | data.frame | NULL"
#
test_mult = function(x){
# x must be either:
# i) a numeric vector of length at least 2
# ii) a square character matrix
# iii) an integer scalar (vector of length 1)
check_arg(x, "numeric vector len(2,) | square character matrix | integer scalar")
invisible(NULL)
}
# OK
test_mult(1)
test_mult(1:2)
test_mult(matrix("ok", 1, 1))
# Not OK, notice the very detailed error messages
try(test_mult(matrix("bonjour", 1, 2)))
try(test_mult(1.1))
#
# X) Multiple arguments
#
# You can check multiple arguments at once if they have the same type.
# You can add the type where you want but it must be a character literal.
# You can check up to 10 arguments with the same type.
test_multiarg = function(xlog1, xlog2, xnum1, xnum2, xnum3){
# checking the logicals
check_arg(xlog1, xlog2, "logical scalar")
# checking the numerics
# => Alternatively, you can add the type first
check_arg("numeric vector", xnum1, xnum2, xnum3)
invisible(NULL)
}
# Let's throw some errors
try(test_multiarg(xlog2 = 4))
try(test_multiarg(xnum3 = "test"))
#
# XI) Multiple sub-stypes
#
# For atomic arguments (like vector or matrices),
# you can check the type of underlying data: is it integer, numeric, etc?
# There are five simple sub-types:
# - integer
# - numeric
# - factor
# - logical
# - loose logical: either TRUE/FALSE, either 0/1
#
# If you require that the data is of one sub-type only:
# - a) if it's one of the simple sub-types: add the keyword directly in the type
# - b) otherwise: add the sub-type in parentheses
#
# Note that the parentheses MUST follow the main class directly.
#
# Example:
# - a) "integer scalar"
# - b) "scalar(Date)"
#
# If you want to check multiple sub-types: you must add them in parentheses.
# Again, the parentheses MUST follow the main class directly.
# Examples:
# "vector(character, factor)"
# "scalar(integer, logical)"
# "matrix(Date, integer, logical)"
#
# In check_set_arg, you can use the keyword "conv" to convert to the
# desired type
#
test_multi_subtypes = function(x, y){
check_arg(x, "scalar(integer, logical)")
check_arg(y, "vector(character, factor, Date)")
invisible(NULL)
}
# What follows doesn't work
try(test_multi_subtypes(x = 5.5))
# Note that it works if x = 5
# (for check_arg 5 is integer although is.integer(5) returns FALSE)
test_multi_subtypes(x = 5)
try(test_multi_subtypes(y = 5.5))
# Testing the "conv" keyword:
test_conv = function(x, type){
check_set_arg(x, .type = type)
x
}
class(test_conv(5L, "numeric scalar conv"))
class(test_conv(5, "integer scalar conv"))
class(test_conv(5, "integer scalar"))
# You can use the "conv" keyword in multi-types
# Remember that types are checked in ORDER! (see the behavior)
test_conv(5:1, "vector(logical, character conv)")
test_conv(c(TRUE, FALSE), "vector(logical, character conv)")
#
# XII) Nested checking: using .up
#
# Say you have two user level functions
# But you do all the computation in an internal function.
# The error message should be at the level of the user-level function
# You can use the argument .up to do that
#
sum_fun = function(x, y){
my_internal(x, y, sum = TRUE)
}
diff_fun = function(x, y){
my_internal(x, y, sum = FALSE)
}
my_internal = function(x, y, sum){
# The error messages will be at the level of the user-level functions
# which are 1 up the stack
check_arg(x, y, "numeric scalar mbt", .up = 1)
if(sum) return(x + y)
return(x - y)
}
# we check it works
sum_fun(5, 6)
diff_fun(5, 6)
# Let's throw some errors
try(sum_fun(5))
try(diff_fun(5, 1:5))
# The errors are at the level of sum_fun/diff_fun although
# the arguments have been checked in my_internal.
# => much easier for the user to understand the problem
#
# XIII) Using check_set_arg to check and set list defaults
#
# Sometimes it is useful to have arguments that are themselves
# list of arguments.
# Witch check_set_arg you can check the arguments nested in lists
# and easily set default values at the same time.
#
# When you check a list element, you MUST use the syntax argument$element
#
# Function that performs a regression then plots it
plot_cor = function(x, y, lm.opts = list(), plot.opts = list(), line.opts = list()){
check_arg(x, y, "numeric vector")
# First we ensure the arguments are lists
check_arg(lm.opts, plot.opts, line.opts, "named list")
# The linear regression
lm.opts$formula = y ~ x
reg = do.call("lm", lm.opts)
# plotting the correlation, with defaults
check_set_arg(plot.opts$main, "character scalar NULL{'Correlation between x and y'}")
# you can use variables created in the function when setting the default
x_name = deparse(substitute(x))
check_set_arg(plot.opts$xlab, "character scalar NULL{x_name}")
check_set_arg(plot.opts$ylab, "character scalar NULL{'y'}")
# we restrict to only two plotting types: p or h
check_set_arg(plot.opts$type, "NULL{'p'} match(p, h)")
plot.opts$x = x
plot.opts$y = y
do.call("plot", plot.opts)
# with the fit
check_set_arg(line.opts$col, "NULL{'firebrick'}") # no checking but default setting
check_set_arg(line.opts$lwd, "integer scalar GE{0} NULL{2}") # check + default
line.opts$a = reg
do.call("abline", line.opts)
}
sepal_length = iris$Sepal.Length ; y = iris$Sepal.Width
plot_cor(sepal_length, y)
plot_cor(sepal_length, y, plot.opts = list(col = iris$Species, main = "Another title"))
# Now throwing errors
try(plot_cor(sepal_length, y, plot.opts = list(type = "l")))
try(plot_cor(sepal_length, y, line.opts = list(lwd = -50)))
#
# XIV) Checking '...' (dot-dot-dot)
#
# You can also check the '...' argument if you expect all objects
# to be of the same type.
#
# To do so, you MUST place the ... in the first argument of check_arg
#
sum_check = function(...){
# we want each element of ... to be numeric vectors without NAs
# we want at least one element to be there (mbt)
check_arg(..., "numeric vector mbt")
# once the check is done, we apply sum
sum(...)
}
sum_check(1:5, 5:20)
# Now let's compare the behavior of sum_check() with that of sum()
# in the presence of errors
x = 1:5 ; y = pt
try(sum_check(x, y))
try(sum(x, y))
# As you can see, in the first call, it's very easy to spot and debug the problem
# while in the second call it's almost impossible
#
# XV) Developer mode
#
# If you're new to check_arg, given the many types available,
# it's very common to make mistakes when creating check_arg calls.
# The developer mode ensures that any problematic call is spotted
# and the problem is clearly stated
#
# Note that since this mode ensures a detailed cheking of the call
# it is thus a strain on performance and should be always turned off
# otherwise needed.
#
# Setting the developer mode on:
setDreamerr_dev.mode(TRUE)
# Creating some 'wrong' calls => the problem is pinpointed
test_err1 = function(x) check_arg(x, "integer scalar", "numeric vector")
try(test_err1())
test_err2 = function(...) check_arg("numeric vector", ...)
try(test_err2())
test_err3 = function(x) check_arg(x$a, "numeric vector")
try(test_err3())
test_err4 = function(x) check_arg(x, "numeric vector integer")
try(test_err4())
# Setting the developer mode off:
setDreamerr_dev.mode(FALSE)
#
# XVI) Using check_value
#
# The main function for checking arguments is check_arg.
# But sometimes you only know if an argument is valid after
# having perfomed some modifications on it.
# => that's when check_value kicks in.
#
# It's better with an example.
#
# In this example we'll construct a plotting function
# using a formula, with a rock-solid argument checking.
#
# Plotting function, but using a formula
# You want to plot only numeric values
plot_fml = function(fml, data, ...){
# We first check the arguments
check_arg(data, "data.frame mbt")
check_arg(fml, "ts formula mbt var(data)", .data = data)
# We extract the values of the formula
y = fml[[2]]
x = fml[[3]]
# Now we check that x and y are valid => with check_value
# We also use the possibility to assign the value of y and x directly
# We add a custom message because y/x are NOT arguments
check_set_value(y, "evalset numeric vector", .data = data,
.message = "In the argument 'fml', the LHS must be numeric.")
check_set_value(x, "evalset numeric vector", .data = data,
.message = "In the argument 'fml', the RHS must be numeric.")
# The dots => only arguments to plot are valid
args_ok = c(formalArgs(plot.default), names(par()))
validate_dots(valid_args = args_ok, stop = TRUE)
# We also set the xlab/ylab
dots = list(...) # dots has a special meaning in check_value (no need to pass .message)
check_set_value(dots$ylab, "NULL{deparse(fml[[2]])} character vector conv len(,3)")
check_set_value(dots$xlab, "NULL{deparse(fml[[3]])} character vector conv len(,3)")
dots$y = y
dots$x = x
do.call("plot", dots)
}
# Let's check it works
plot_fml(Sepal.Length ~ Petal.Length + Sepal.Width, iris)
plot_fml(Sepal.Length ~ Petal.Length + Sepal.Width, iris, xlab = "Not the default xlab")
# Now let's throw some errors
try(plot_fml(Sepal.Length ~ Species, iris))
try(plot_fml(Sepal.Length ~ Petal.Length, iris, xlab = iris))
try(plot_fml(Sepal.Length ~ Petal.Length, iris, xlab = iris$Species))
Run the code above in your browser using DataLab