Search code examples
rparameter-passingmetaprogrammingdefault-value

ISO a good way to let a function accept a mix of supplied arguments, arguments from a list, and defaults


I would like to have a function accept arguments in the usual R way, most of which will have defaults. But I would also like it to accept a list of named arguments corresponding to some or some or all of the formals. Finally, I would like arguments supplied to the function directly, and not through the list, to override the list arguments where they conflict.

I could do this with a bunch of nested if-statements. But I have a feeling there is some elegant, concise, R-ish programming-on-the-language solution -- probably multiple such solutions -- and I would like to learn to use them. To show the kind of solution I am looking for:

> arg_lst <- list(x=0, y=1)
> fn <- function(a_list = NULL, x=2, y=3, z=5, ...){
   <missing code>
   print(c(x, y, z))
  }

> fn(a_list = arg_list, y=7)

Desired output:

x  y  z
0  7  5

Solution

  • I like a lot about @jdobres's approach, but I don't like the use of assign and the potential scoping breaks.

    I also don't like the premise, that a function should be written in a special way for this to work. Wouldn't it be better to write a wrapper, much like do.call, to work this way with any function? Here is that approach:

    Edit: solution based off of purrr::invoke

    Thinking a bit more about this, purrr::invoke almost get's there - but it will result in an error if a list argument is also passed to .... But we can make slight modifications to the code and get a working version more concisely. This version seems more robust.

    library(purrr)
    h_invoke = function (.f, .x = NULL, ..., .env = NULL) {
        .env <- .env %||% parent.frame()
        args <- c(list(...), as.list(.x))  # switch order so ... is first
        args = args[!duplicated(names(args))] # remove duplicates
        do.call(.f, args, envir = .env)
    }
    
    h_invoke(fn, arg_list, y = 7)
    # [1] 0 7 5
    

    Original version borrowing heavily from jdobres's code:

    hierarchical_do_call = function(f, a_list = NULL, ...){
       formal_args = formals() # get the function's defined inputs and defaults
       formal_args[names(formal_args) %in% c('f', 'a_list', '...')] = NULL # remove these two from formals
       supplied_args <- as.list(match.call())[-1] # get the supplied arguments
       supplied_args[c('f', 'a_list')] = NULL # ...but remove the argument list and the function
       a_list[names(supplied_args)] = supplied_args
       do.call(what = f, args = a_list)
    }
    
    fn = function(x=2, y=3, z=5) {
      print(c(x, y, z))
    }
    
    arg_list <- list(x=0, y=1)
    hierarchical_do_call(f = fn, a_list = arg_list, y=7)
    # x  y  z
    # 0  7  5