andrewH
andrewH

Reputation: 2321

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

Upvotes: 0

Views: 50

Answers (3)

Gregor Thomas
Gregor Thomas

Reputation: 146249

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

Upvotes: 3

jdobres
jdobres

Reputation: 11957

I'm not sure how "elegant" this is, but here's my best attempt to satisfy the OP's requirements. The if/else logic is actually pretty straightforward (no nesting needed, per se). The real work is in collecting and sanitizing the three different input types (formal defaults, the list object, and any supplied arguments).

fn <- function(a_list = NULL, x = 2, y = 3, z = 5, ...) {

  formal_args <- formals() # get the function's defined inputs and defaults
  formal_args[names(formal_args) %in% c('a_list', '...')] <- NULL # remove these two from formals
  supplied_args <- as.list(match.call())[-1] # get the supplied arguments
  supplied_args['a_list'] <- NULL # ...but remove the argument list

  # for each uniquely named item among the 3 inputs (argument list, defaults, and supplied args):
  for (i in unique(c(names(a_list), names(formal_args), names(supplied_args)))) {
    if (!is.null(supplied_args[[i]])) {
      assign(i, supplied_args[[i]])
    } else if (!is.null(a_list[[i]])) {
      assign(i, a_list[[i]])
    }
  }

    print(c(x, y, z))
}

arg_lst <- list(x = 0, y = 1)
fn(a_list = arg_lst, y=7)

[1] 0 7 5

With a little more digging into R's meta-programming functions, it's actually possible to pack this hierarchical assignment into its own function, which is designed to operate on the function environment that called it. This makes it easier to reuse this functionality, but it definitely breaks scope and should be considered dangerous.

The "hierarchical assignment" function, mostly the same as before:

hierarchical_assign <- function(a_list) {

  formal_args <- formals(sys.function(-1)) # get the function's defined inputs and defaults
  formal_args[names(formal_args) %in% c('a_list', '...')] <- NULL # remove these two from formals
  supplied_args <- as.list(match.call(sys.function(-1), sys.call(-1)))[-1] # get the supplied arguments
  supplied_args['a_list'] <- NULL # ...but remove the argument list

  # for each uniquely named item among the 3 inputs (argument list, defaults, and supplied args):
  for (i in unique(c(names(a_list), names(formal_args), names(supplied_args)))) {
    if (!is.null(supplied_args[[i]])) {
      assign(i, supplied_args[[i]], envir = parent.frame())
    } else if (!is.null(a_list[[i]])) {
      assign(i, a_list[[i]], envir = parent.frame())
    }
  }
}

And the usage. Note that the the calling function must have an argument named a_list, and it must be passed to hierarchical_assign.

fn <- function(a_list = NULL, x = 2, y = 3, z = 5, ...) {

    hierarchical_assign(a_list)

    print(c(x, y, z))
}

[1] 0 7 5

Upvotes: 2

JMenezes
JMenezes

Reputation: 1059

I think do.call() does exactly what you want. It accepts a function and a list as arguments, the list being arguments for the functions. I think you will need a wrapper function to create this behavior of "overwriting defaults"

Upvotes: 0

Related Questions