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)