Home > other >  How to ensure that parameters have been called by name and not by position?
How to ensure that parameters have been called by name and not by position?

Time:10-21

I'm maintaining a package centered on a single function with a few mandatory parameters, along with a lot of optional parameters.

As my function matures, the order of optional parameters is changing, and therefore calling them by order will lead to breaking changes.

I would like to throw a warning/error (not sure what is best) if these latter parameters are called by position rather than by name.

Here is some pseudo-code with expected output:

crosstable = function(data, cols=NULL, ..., by=NULL, opt1=FALSE, opt2=FALSE, opt3=FALSE){
    warn_if_unnamed(by)
    stop_if_unnamed(opt1)
    stop_if_unnamed(opt2)
    stop_if_unnamed(opt3)
    doStuff(data, cols, by, opt1, opt2, opt3)
}
crosstable(mtcars, c(cyl, am), by=vs, opt3=TRUE) #OK
crosstable(mtcars, c(cyl, am), by=vs, TRUE)      #error as `opt1` might become `opt56` in the future
crosstable(mtcars, c(cyl, am), vs, opt2=TRUE)    #warning (?) as `by` will not move but it would be clearer

How can I achieve this?

EDIT:

Thanks to @user2554330 and to some other SO post (here), I finally got it working, although it won't work if used with a pipe:

warn_if_unnamed <- function(argname){
    .call = sys.call(-1)
    f = get(as.character(.call[[1]]), mode="function", sys.frame(-2))
    mc = names(as.list(match.call(definition=f, call=.call))) #https://stackoverflow.com/a/17257053/3888000
    sc = names(as.list(.call))
    if(argname %in% mc && !argname %in% sc){
        warning(argname," is referenced by position, not name")
    }
}
myfun = function(x, y=NULL, opt1=FALSE, opt2=FALSE, opt3=FALSE){
    warn_if_unnamed("opt1")
    warn_if_unnamed("opt2")
    warn_if_unnamed("opt3")
    invisible()
}
myfun(1, 2)
myfun(1, 2, T, opt2=1)
#> Warning in warn_if_unnamed("opt1"): opt1 is referenced by position, not name
myfun(1, 2, opt1=T, 1, opt3)
#> Warning in warn_if_unnamed("opt2"): opt2 is referenced by position, not name
#> Warning in warn_if_unnamed("opt3"): opt3 is referenced by position, not name
myfun(1, 2, opt2=T, 1, opt3)
#> Warning in warn_if_unnamed("opt1"): opt1 is referenced by position, not name
#> Warning in warn_if_unnamed("opt1"): opt3 is referenced by position, not name

Created on 2021-10-20 by the reprex package (v2.0.1)

I'll probably make some refactoring to gather the warnings into a single one though.

PS: The last line looks like a bug in reprex().

CodePudding user response:

You can use the sys.call() function to see how your function was called, and match.call()to see how R matches arguments to parameters. So code for warn_if_unnamed(by) would be:

if ("by" %in% names(as.list(match.call())) &&
   !"by" %in% names(as.list(sys.call())))
  warning("'by' should be named")

It's possible to put this in a function; you'd need to use the where argument to sys.call() and match.call() to look at the arguments to the caller of your function instead of the arguments to warn_if_unnamed itself.

CodePudding user response:

This could be a warn_if_unnamed function:

warn_if_unnamed <- function(argname){
  arguments <- as.list(sys.call(which = -1)) # get arguments of sys.call
  arg_name <- deparse(substitute(argname))# get variable name
  arg_named <- arg_name %in% names(arguments)
  if(!arg_named){
    warning(arg_name," is referenced by position, not name")
  }
}

Example of use:

myfun <- function( arg1,arg2=2,arg3=3 ) {
  warn_if_unnamed(arg2)
}
myfun(1,2)

#In warn_if_unnamed(arg2) : arg2 is referenced by position, not name

error if unnamed could be analogous with error() instead of warning()

CodePudding user response:

func <- function(data, cols = NULL, ...) {
  opt_args <- list(...)
  
  if(length(opt_args) > 0 && is.null(names(opt_args))) {
    stop("Optional arguments must be named")
  }
  
  allowed_args <- c("opt1", "opt2")
  if(length(setdiff(names(opt_args), allowed_args)) > 0) {
    warning("Additional unknown arguments are ignored")
  }
  
  opt_args
}

# ok
func(iris, c("Sepal.Length", "Sepal.Width"))
#> list()
func(iris, c("Sepal.Length", "Sepal.Width"), opt1 = "foo")
#> $opt1
#> [1] "foo"

# warning 
func(iris, c("Sepal.Length", "Sepal.Width"), opt3 = "foo")
#> Warning in func(iris, c("Sepal.Length", "Sepal.Width"), opt3 = "foo"):
#> Additional unknown arguments are ignored
#> $opt3
#> [1] "foo"

# error
func(iris, c("Sepal.Length", "Sepal.Width"), "foo")
#> Error in func(iris, c("Sepal.Length", "Sepal.Width"), "foo"): Optional arguments must be named

Created on 2021-10-20 by the reprex package (v2.0.1)

  • Related