Search code examples
rlistfunctionuser-inputdefault-value

Multiple similar inputs to an R function - problems with providing them using lists


I have the following problem: I want to write a function that is supposed to get multiple similar sets of inputs, lets say:

FUN <- function(part1.x, part1.y, part1.z, part2.x, part2.y, part2.z){}

So the variables for each part are the same, but they can have different values. My first attempt was to just use them like that, but the number of inputs grew and it looked quite convoluted. My next idea was to store the variables as vectors and use the position of the elements to indicate which part they belong to like

FUN <- function(x, y, z){}, where each input is a vector c[part1, part2]. That was better, but I found it difficult to swap inputs between parts and it feels a bit confusing.

The idea I liked the most handling-wise and in terms of user-friendliness is to store a list with the inputs for each part, such as

FUN <- function(part1 = list(x, y, z), part2 = list(x, y, z){}

The problem here is that it does not seem possible to combine default values with user input, so when the function reads:

FUN <- function(part1 = list(x = 1, y = TRUE, z = "abc"), part2 = list(x = 2, y = FALSE, z = "bcd")){}

and the user does FUN(part1 = list(y = FALSE), part2 = list(x = 2)), then for part1$x, part1$z, part2$y and part2$z the defaults are ignored and the variables are nonexistent as the whole list gets overwritten.

As a workaround I wrote a function for each part which is basically just there to store and return the default values:

part1_fun <- function(x = 1, y = TRUE, z = "abc"){
  return(formals(part1_fun))
} 

part2_fun <- function(x = 2, y = FALSE, z = "bcd"){
  return(formals(part2_fun))
} 

Which are then called within the main function to retrieve the defaults, which are then combined by name equality with the provided input:

FUN <- function(part1_args, part2_args){
  part1_default <- part1_fun(part1_args)
  part2_default <- part2_fun(part2_args)
  
  common_names_part1 <- intersect(names(part1_args), names(part1_default ))
  common_names_part2 <- intersect(names(part2_args), names(part2_default ))
  
  part1_input <- part1_default
  part1_input[common_names_part1] <- part1_args[common_names_part1]
  part2_input <- part2_default
  part2_input[common_names_part2] <- part2_args[common_names_part2]
 
  print(part1_input)
  print(part2_input)
   
}

FUN(part1_args = list(x = 3, z = "a"), part2_args = list(y = TRUE))

This works (but it feels like a hack) as it then uses all the values (default and user-provided), however one downside I saw already is that it does not catch misspelled input and just ignores it if I don't catch that separately. Another is that I currently do not see a way to get match.arg to work in this scenario to allow for shorter input strings. So when z can be one of two strings part1_fun <- function(x = 1, y = TRUE, z = c("abc", zyx")){, then adding z = match.arg(z) in this function does not lead to the user input FUN(part1_args = list(z = "a")) being turned to z == "abc". Instead it stays "a".

So my questions:

  1. What is the best way to handle multiple similar input values as in my case? I feel like I probably missed something. Is there a go-to way?

And if that is not the case:

  1. Has my approach more downsides than the ones I already mentioned - and how to solve the ones I spotted best?

I found this somewhat related posts Get all Parameters as List and Extract the elements of list as individual arguments within custom function in r but I cannot get it to work and also wanted to make sure that I am on the right track at all...

Thank you!


Solution

  • You can pass the arguments to your helper functions using do.call(), then return them. This will fill in defaults and throw appropriate errors.

    part1_fun <- function(x = 1, y = TRUE, z = c("abc", "zyx")){
      z <- match.arg(z)
      list(x = x, y = y, z = z)
    } 
    
    part2_fun <- function(x = 2, y = FALSE, z = "bcd"){
      list(x = x, y = y, z = z)
    } 
    
    FUN <- function(part1_args, part2_args){
      part1_input <- do.call(part1_fun, part1_args)
      part2_input <- do.call(part2_fun, part2_args)
     
      print(part1_input)
      print(part2_input)   
    }
    
    FUN(
      part1_args = list(x = 3, z = "zyx"), 
      part2_args = list(y = TRUE)
    )
    # $x
    # [1] 3
    # 
    # $y
    # [1] TRUE
    # 
    # $z
    # [1] "zyx"
    # 
    # $x
    # [1] 2
    # 
    # $y
    # [1] TRUE
    # 
    # $z
    # [1] "bcd"
    

    Misspelled argument error:

    FUN(
      part1_args = list(xxy = 3, z = "zyx"), 
      part2_args = list(y = TRUE)
    )
    # Error in (function (x = 1, y = TRUE, z = c("abc", "zyx"))  : 
    #   unused argument (xxy = 3)
    

    match.arg() error:

    FUN(
      part1_args = list(x = 3, z = "x"), 
      part2_args = list(y = TRUE)
    )
    # Error in match.arg(z) : 'arg' should be one of “abc”, “zyx”