Reputation: 3993
I am programming in R and need to check whether all the arguments provided by the user in a list params
are valid arguments to a function f
. The function should return an error if any named elements of params
do not match the argument names of f
. The way it is currently implemented is:
if (is.null(names(params)) || any(!names(params) %in% names(formals(f)))) {
stop("names of params must match arguments of f")
}
I have run into a problem testing this code with the function caret::train
. The function train
only has a single formal argument, x
, so names(formals(caret::train))
returns c('x','...')
. However, the default S3 method for caret::train
has additional formal arguments. How can I programmatically test whether the named list input by the user matches the function arguments, if they are only arguments for one of the methods and not for the function itself? This should be a general solution which should work for any function, not just train
.
my_fun <- function(f, params) {
if (is.null(names(params)) || any(!names(params) %in% names(formals(f)))) {
stop("names of params must match arguments of f")
}
do.call(f, params)
}
library(caret)
# my_fun returns error: names of params must match arguments of f
my_fun(f = caret::train, params = list(x = data.frame(x = 1:10), y = rep(1,10), method = 'rf'))
# caret::train returns error: argument "y" is missing, with no default
my_fun(f = caret::train, params = list(x = data.frame(x = 1:10)))
Upvotes: 0
Views: 194
Reputation: 19541
The question is:
You can get the default method for the function passed using the getS3method
function.
my_fun <- function(f, params) {
# Look for a default method first
f.default <- try(getS3method(deparse(substitute(f)), "default"), silent=TRUE)
if(class(f.default)=="try-error")
stop("There is no default method for f.")
# Get the formals for the default method
f.args <- formalArgs(f.default)
# Excessive arguments
excessive.pargs <- setdiff(names(params), f.args)
if (length(excessive.pargs)>0)
stop("You have extra arguments that don't match arguments of f: ", excessive.pargs)
# Continue with no error (except for missing arguments)
do.call(f, params)
}
Test it:
library(caret)
my_fun(f = train, params = list(x = data.frame(x = 1:10)))
#Error in my_fun(f = train, params = list(x = data.frame(x = 1:10))) :
#argument "y" is missing, with no no default
my_fun(f = train, params = list(x = data.frame(x = 1:10), z="Invalid argument"))
#Error in my_fun(f = train, params = list(x = data.frame(x = 1:10), z = "Invalid argument")) :
#You have extra arguments that don't match arguments of f: z
my_fun(f = train, params = list(x = data.frame(x = 1:10), y = rep(1,10), method = 'rf'))
# Works
Test it on a function with no default method:
my_fun(f = lm, params = list(data=iris))
#Error in my_fun(f = lm, params = list(data = iris)) :
#There is no default method for f.
Upvotes: 1
Reputation: 35392
I think this won't be easy. You can check for S3, as I do below, and that mostly works. However, this does not consider the other OOP systems that R has. And, even this implementation is not that stable, e.g. this won't work if you use ::
notation (i.e. train
works but caret::train
doesn't). That last bit is because getS3method
doesn't work with ::
notation, no idea why.
my_fun <- function(f, params, env = parent.frame()) {
# check for S3 generic
if (isS3stdGeneric(f)) {
s <- deparse(substitute(f))
dispatch_arg <- formalArgs(f)[1]
classes_to_check <- c(class(params[[dispatch_arg]]), 'default')
for (i in seq_along(classes_to_check)) {
f <- getS3method(s, classes_to_check[i], optional = TRUE, parent.frame(n = 2))
if (is.function(f)) break
}
}
if (is.null(names(params)) || !all(names(params) %in% formalArgs(f))) {
stop("names of params must match arguments of f", call. = FALSE)
}
do.call(f, params)
}
Examples:
library(caret)
my_fun(f = train, params = list(x = data.frame(x = 1:10), y = rep(1,10), method = 'rf'))
# works
my_fun(f = train, params = list(x = data.frame(x = 1:10)))
# argument "y" is missing, with no default
Upvotes: 2