Home > Software design >  pass a function a vector or undefined number of arguments
pass a function a vector or undefined number of arguments

Time:12-31

I want to be able to pass a function an undefined number of arguments via ... but also to be able to pass it a vector. Here is a silly example:

library(tidyverse)
df <- data.frame(gear = as.character(unique(mtcars$gear)),
                 id = 1:3)
myfun <- function(...) {
  ids_lst <- lst(...)
  df2 <- bind_rows(map(ids_lst, function(x) 
    mtcars %>% 
      filter(gear == x) %>% 
      select(mpg)), .id = "gear") %>% 
    left_join(df)
  df2
}
#these all work:
myfun(3)
myfun(3, 4)
myfun(3, 4, 5)

Passing it a vector doesn't work though:

myvector <- unique(mtcars$gear)
myfun(myvector)

The problem is because of the way the function collects the arguments and how it returns them:

myfun_lst <- function(...) {
  ids_lst <- lst(...)
  ids_lst
}
myfun_lst(3, 4, 5)
# $`3`
# [1] 3

# $`4`
# [1] 4

# $`5`
# [1] 5

myfun_lst(myvector)
# $myvector
# [1] 4 3 5

I thought a fix would be to test if the input is a vector, something like:

myfun_final <- function(...) {
  if(is.vector(...) & !is.list(...)) {
    ids_lst <- as.list(...)
    names(ids_lst) <- (...)
  } else { 
    ids_lst <- lst(...)
  }
  df2 <- bind_rows(map(ids_lst, function(x) 
    mtcars %>% 
      filter(gear == x) %>% 
      select(mpg)), .id = "gear") %>% 
    left_join(df)
  df2
}

Now, passing the function a vector works but collecting the arguments doesn't:

myfun_final(3, 4, 5)
myfun_final(myvector)

What is a good way to solve this? Thanks

CodePudding user response:

How about testing if ... is of length 1 and if the only argument passed through is a vector? If not so, then consider ... a list of scalers and capture them with lst(...).

myfun_final <- function(...) {
  if (...length() == 1L && is.vector(..1))
    ids_lst <- `names<-`(..1, ..1)
  else
    ids_lst <- lst(...)
  
  df2 <- bind_rows(map(ids_lst, function(x) 
    mtcars %>% 
      filter(gear == x) %>% 
      select(mpg)), .id = "gear") %>% 
    left_join(df)
  df2
}

Test

> myfun_final(3)
Joining, by = "gear"
   gear  mpg id
1     3 21.4  2
2     3 18.7  2
3     3 18.1  2
4     3 14.3  2
5     3 16.4  2
6     3 17.3  2
7     3 15.2  2
8     3 10.4  2
9     3 10.4  2
10    3 14.7  2
11    3 21.5  2
12    3 15.5  2
13    3 15.2  2
14    3 13.3  2
15    3 19.2  2
> myfun_final(3,4,5)
Joining, by = "gear"
   gear  mpg id
1     3 21.4  2
2     3 18.7  2
3     3 18.1  2
4     3 14.3  2
5     3 16.4  2
6     3 17.3  2
7     3 15.2  2
8     3 10.4  2
9     3 10.4  2
10    3 14.7  2
11    3 21.5  2
12    3 15.5  2
13    3 15.2  2
14    3 13.3  2
15    3 19.2  2
16    4 21.0  1
17    4 21.0  1
18    4 22.8  1
19    4 24.4  1
20    4 22.8  1
21    4 19.2  1
22    4 17.8  1
23    4 32.4  1
24    4 30.4  1
25    4 33.9  1
26    4 27.3  1
27    4 21.4  1
28    5 26.0  3
29    5 30.4  3
30    5 15.8  3
31    5 19.7  3
32    5 15.0  3
> myfun_final(c(3,4,5))
Joining, by = "gear"
   gear  mpg id
1     3 21.4  2
2     3 18.7  2
3     3 18.1  2
4     3 14.3  2
5     3 16.4  2
6     3 17.3  2
7     3 15.2  2
8     3 10.4  2
9     3 10.4  2
10    3 14.7  2
11    3 21.5  2
12    3 15.5  2
13    3 15.2  2
14    3 13.3  2
15    3 19.2  2
16    4 21.0  1
17    4 21.0  1
18    4 22.8  1
19    4 24.4  1
20    4 22.8  1
21    4 19.2  1
22    4 17.8  1
23    4 32.4  1
24    4 30.4  1
25    4 33.9  1
26    4 27.3  1
27    4 21.4  1
28    5 26.0  3
29    5 30.4  3
30    5 15.8  3
31    5 19.7  3
32    5 15.0  3

CodePudding user response:

Of course you can change your function so that it will work with both, regular arguments myfun(3, 4, 5) and a vector myfun(myvector), as shown in the answer above.

Another option is that you make use of argument splicing by unquoting with the bang bang bang operator !!!. This operator is only supported in certain {rlang} and {tidyverse} functions. In your example you evaluate the dots ... inside purrr::map which supports argument splicing. Therefore there might not be the need to rewrite your function:

library(tidyverse)

# your original function:
myfun <- function(...) {
        ids_lst <- lst(...)
        df2 <- bind_rows(map(ids_lst, function(x) 
                mtcars %>% 
                        filter(gear == x) %>% 
                        select(mpg)), .id = "gear") %>% 
                left_join(df)
        df2
}

myvector <- unique(mtcars$gear)

myfun(!!! myvector) # works

#> Joining, by = "gear"
#>    gear  mpg id
#> 1     4 21.0  1
#> 2     4 21.0  1
#> 3     4 22.8  1
#> 4     4 24.4  1
#> 5     4 22.8  1
#> 6     4 19.2  1
#> 7     4 17.8  1
#> 8     4 32.4  1
#> 9     4 30.4  1
#> 10    4 33.9  1
#> ...


myfun(3, 4, 5) # works

#> Joining, by = "gear"
#>    gear  mpg id
#> 1     3 21.4  2
#> 2     3 18.7  2
#> 3     3 18.1  2
#> 4     3 14.3  2
#> 5     3 16.4  2
#> 6     3 17.3  2
#> 7     3 15.2  2
#> 8     3 10.4  2
#> 9     3 10.4  2
#> 10    3 14.7  2
#> ...

Created on 2021-12-30 by the reprex package (v0.3.0)

You can read more about unquoting with the bang bang bang operator here.

Finally, you should think about the users of your function. If you are the only user then choose whatever suits you. In case there are other users you should think about how they expect the function to work. Probably users don't expect a function to work with several arguments and at the same time, alternatively, by providing those arguments in a vector. In the tidyverse argument splicing with !!! is a well established concept. In base R we would usually use do.call("myfun", as.list(myvector)) to achieve something similar.

  • Related