waferthin
waferthin

Reputation: 1602

R: check mandatory/required arguments are provided

I'd like to be able to have a way to check all arguments without defaults are specified when calling a function in R. This seems like a sensible thing to do to me, as it avoids a function failing later on (which could be after a lot of processing) when a value is found to be missing.

One way to accomplish this task would be to write a if (missing(arg)) statement for each argument, but this requires keeping the arguments for the function and the above statements consistent with each other, so I'm looking for a better solution.

At present I use the following function, which works in most cases, but not all.

# check for required arguments by getting arguments for the
# definition of the calling function and comparing to the arguments 
# in the function call
check_required_args <- function () {
    def    <- sys.function(-1)
    f_args <- formals(def)
    f_args <- f_args[sapply(f_args, is.name)]  # remove arguments with defaults
    f_args <- names(f_args)
    f_args <- f_args[f_args != '...'] # remove ellipsis argument if present

    call   <- match.call(definition=def, call=sys.call(-1))
    f_name <- call[1]
    c_args <- names(as.list(call[-1]))

    for(n in f_args) {
        if (!n %in% c_args) {
            stop("Argument '", n, "' missing from call to function ", 
                f_name, "()", call.=FALSE)
        }
    }
}

f <- function(a, b, c=2) check_required_args()
f(a=1) # should fail (missing argument b)
f(2, 3) # should work
f(2, c=5) # should fail (missing argument b)
f(2, 3, 4) # should work


f <- function(a, b, ...) f2(a, b, ...)
f2 <- function(a, b, c, ...) check_required_args()

f2(a=1, b=2, c=3) # should work
f2(a=1, b=2) # should fail (missing argument c for function f2)
f(a=1, b=2, c=3) # should work
f(a=1, b=2) # should fail  (missing argument c for function f2)

Can this function be improved to work in all of these cases? If not is there a more appropriate solution to perform this check?

Upvotes: 3

Views: 1714

Answers (1)

Roland
Roland

Reputation: 132676

Maybe this?

check_required_args <- function (fun = sys.function(-1), ncall = 3) {
  f_args <- formals(fun)
  f_args <- f_args[vapply(f_args, is.symbol, FUN.VALUE = TRUE)]
  f_args <- names(f_args)
  f_args <- setdiff(f_args, "...")
  test <- vapply(f_args, 
                 function(x) missingArg(as.name(x), envir = parent.frame(ncall), eval = TRUE), 
                 FUN.VALUE = TRUE)
  stopifnot(!any(test))
  return(invisible(NULL))     
}


f <- function(a, b, c=2) {
  check_required_args()
  return("Hello!")
}
f(a=1) # should fail (missing argument b)
#Error: !any(test) is not TRUE

f(2, 3) # should work
#[1] "Hello!"

f(2, c=5) # should fail (missing argument b)
# Error: !any(test) is not TRUE

f(2, 3, 4) # should work
#[1] "Hello!"

x <- 1
f(a=x, 3)
#[1] "Hello!"


f <- function(a, b, ...) f2(a, b, ...)
f2 <- function(a, b, c, ...) {
  check_required_args()
  return("Hello!")
}

f2(a=1, b=2, c=3) # should work
#[1] "Hello!"

f2(a=1, b=2) # should fail (missing argument c for function f2)
#Error: !any(test) is not TRUE 

f(a=1, b=2, c=3) # should work
#[1] "Hello!"

f(a=1, b=2) # should fail  (missing argument c for function f2)
#Error: !any(test) is not TRUE 

Edit:

You might want to use get to check for existence:

check_required_args <- function (fun = sys.function(-1), ncall = 3) {
  f_args <- formals(fun)
  f_args <- f_args[vapply(f_args, is.symbol, FUN.VALUE=TRUE)]
  f_args <- names(f_args)
  f_args <- setdiff(f_args, "...")
  test <- lapply(f_args, 
                 function(x) {
                   get(x, envir = parent.frame(ncall), inherits = TRUE)
                   return(NULL)
                   })
  #possibly use a for loop instead
  #wrap in tryCatch for customized error messages

}

f <- function(a, b, ...) f2(a, b, ...)
f2 <- function(a, b, c, ...) {
  check_required_args()
  return("Hello!")
}

f(c=2)
#Error in get(x, envir = parent.frame(ncall), inherits = TRUE) : 
#  argument "a" is missing, with no default 

If you don't want check in enclosing frames, set inherits = FALSE.

Upvotes: 4

Related Questions