[This article was first published on R snippets, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Recently I have read a nice post on ensuring that proper arguments are passed to a function using GNU R class system. However, I often need a more lightweight solution to repetitive function argument testing.Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
The alternative idea is to test function arguments against a specified pattern given in a string. The pattern I use has the form:
([argument type][required argument length])+.
The allowed argument types are:
b logical
i integer
d double
n numeric
c complex
s character
f function
l list
a any type
and argument lengths are:
? 0 or 1
* 0 or more
+ 1 or more
n exactly n
So for example pattern “n2f?s+” means that function requires three arguments: one numeric vector of length 2, a function (or nothing) and character vector of any positive length.
The function that performs checking of the arguments against the pattern is given here:
is.valid <- function(rule, …, empty.null = TRUE) {< o:p>
err <- function(type, position, required, found) {< o:p>
e <- FALSE< o:p>
attr(e, “type”) <- type< o:p>
attr(e, “position”) <- position< o:p>
attr(e, “required”) <- required< o:p>
attr(e, “found”) <- found< o:p>
return(e)< o:p>
}< o:p>
stopifnot(require(stringr, quietly = T))< o:p>
arglist <- list(…)< o:p>
if (!(is.character(rule) ||< o:p>
(length(rule) == 1)) ||< o:p>
(nchar(rule) == 0)) {< o:p>
stop(“improper rule: rule must be a nonempty string”)< o:p>
}< o:p>
type <- str_locate_all(rule, “[bidncsfla]”)[[1]]< o:p>
count <- invert_match(type)< o:p>
type <- str_sub(rule, type[,1], type[,2])< o:p>
count <- str_sub(rule, count[,1], count[,2])< o:p>
if (count[1] != “”) {< o:p>
stop(“improper rule: rule must start with type [bidncsfl]”)< o:p>
}< o:p>
count <- count[-1]< o:p>
if (length(count) != length(type)) {< o:p>
stop(“improper rule: number of type and count specifiers must be equal”)< o:p>
}< o:p>
for (i in seq_along(count)) {< o:p>
if ((!count[i] %in% c(“?”, “*”, “+”)) &&< o:p>
(regexpr(“^[1-9][0-9]*$”, count[i]) == –1)) {< o:p>
stop(paste(“improper rule: unrecognized count”,< o:p>
count[i]))< o:p>
}< o:p>
}< o:p>
if (length(type) != length(arglist)) {< o:p>
stop(“improper rule: number of type specifiers must be equal to number of variables”)< o:p>
}< o:p>
for (i in seq_along(count)) {< o:p>
if ((!count[i] %in% c(“?”, “*”, “+”)) &&< o:p>
(regexpr(“^[1-9][0-9]*$”, count[i]) == –1)) {< o:p>
stop(paste(“improper rule: unrecognized count”,< o:p>
count[i]))< o:p>
}< o:p>
if ((count[i] %in% c(“?”, “*”)) &&< o:p>
is.null(arglist[[i]]) &&< o:p>
empty.null) {< o:p>
next< o:p>
}< o:p>
if (!switch(type[i],< o:p>
b = is.logical(arglist[[i]]),< o:p>
i = is.integer(arglist[[i]]),< o:p>
d = is.double(arglist[[i]]),< o:p>
n = is.numeric(arglist[[i]]),< o:p>
c = is.complex(arglist[[i]]),< o:p>
s = is.character(arglist[[i]]),< o:p>
f = is.function(arglist[[i]]),< o:p>
l = is.list(arglist[[i]]),< o:p>
a = TRUE)) {< o:p>
return(err(“type”, i, type[i], typeof(arglist[[i]])))< o:p>
}< o:p>
if (count[i] == “?”) {< o:p>
if (length(arglist[[i]]) > 1) {< o:p>
return(err(“count”, i, count[i],< o:p>
length(arglist[[i]])))< o:p>
}< o:p>
} else if (count[i] == “*”) {< o:p>
# nothing to do – always met< o:p>
} else if (count[i] == “+”) {< o:p>
if (length(arglist[[i]]) == 0) {< o:p>
return(err(“count”, i, count[i],< o:p>
length(arglist[[i]])))< o:p>
}< o:p>
} else {< o:p>
if (length(arglist[[i]]) != as.integer(count[i])) {< o:p>
return(err(“count”, i, as.integer(count[i]),< o:p>
length(arglist[[i]])))< o:p>
}< o:p>
}< o:p>
}< o:p>
return(TRUE)< o:p>
}Its first argument is rule a pattern that arguments should meet. Next the arguments are passed. For example in the following calls:
is.valid(“b1i1d1n1c1s1f1l1a1”,< o:p>
T, 1L, 1.0, 1, 1i, “1”, sin, list(1), raw(1)) # TRUE< o:p>
is.valid(“b1i1d1n1c1s1f1l1a1”,< o:p>
T, 1L, 1.0, 1, 1i, “1”, sin, list(1), raw(2)) # FALSE< o:p>
is.valid(“b1i1d1n1c1s1f1l1a1”,< o:p>
T, 1L, 1.0, 1, 1i, “1”, sin, 1, raw(1)) # FALSEthe first is returns TRUE and second and third return FALSE (in the first there an improper length of last argument and in the second improper variable type). In case when FALSE is returned it contains attributes with diagnostic information on validation error type.
Additionally the function has an optional parameter empty.null. It influences the way it handles variables of length 0 (which are allowed when “?” or “*” argument length constraint is used). If it is set to TRUE then NULL as an argument is accepted as valid. You can see this in action in the following code:
is.valid(“l?”, NULL) # TRUE
is.valid(“l?”, NULL, empty.null = FALSE) # FALSE
In summary is.valid function is a simple and compact way to check type and length of passed arguments and can be considered as an alternative to the approach proposed in the post I have mentioned above.
To leave a comment for the author, please follow the link and comment on their blog: R snippets.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.