qdread
qdread

Reputation: 3993

How to test whether arguments provided by user match formal arguments of a default method of a function?

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.

reproducible example

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

Answers (2)

Edward
Edward

Reputation: 19541

The question is:

How to test whether arguments provided by user match formal arguments of a default method of a function?

Answer:

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

Axeman
Axeman

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

Related Questions