Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unpack ... with missing arguments

Tags:

r

I'm writing a function that accepts a variable number of arguments. Moreover, I would like the user to be able to leave some of these arguments as missing.

Consider just the task of turning the ... into a list of arguments. Here is my first attempt:

f <- function(...) list(...)

This fails:

f(1,,2)
## Error in f(1, , 2) : argument is missing, with no default

I would like the result to be list(1, NULL, 2). (I am not worried about distinguishing f(1,,2) from f(1,NULL,2).)

Here is my second attempt:

g <- function(...)
{
    args <- match.call()
    miss <- vapply(args, identical, NA, quote(expr=))
    args[miss] <- list(NULL)
    args[[1L]] <- quote(list)
    eval.parent(args)
}

This works (sort of):

identical(g(1,,2), list(1, NULL, 2))
## [1] TRUE

However, it fails when forwarding dots:

gg <- function(...) g(...)
identical(gg(1,,2), list(1,NULL,2))
## [1] FALSE

Is there a way to achieve the same results as g, but in a way that allows forwarding dots and without using match.call()?

Edit: Issues with forwarding dots.

It's tempting to try the following:

h <- function(...)
{
    args <- substitute(list(...))
    miss <- vapply(args, identical, NA, quote(expr=))
    args[miss] <- list(NULL)
    eval.parent(args)
}

However, as @lionel points out, there will be issues if you try to forward ... from another function:

hh <- function(...) h(letters, ...)
local({ a <- "foo"; h(a) })
## Error in eval(expr, p) : object 'a' not found

It seems like you either need the expand.dots functionality of match.call, or you need to use a quosure (from the rlang package).

like image 849
Patrick Perry Avatar asked Dec 24 '22 12:12

Patrick Perry


1 Answers

With the rlang package you have the option of ignoring empty arguments when capturing dots:

my_dots <- function(...) {
  rlang::dots_list(..., .ignore_empty = "all")
}

my_dots(1, , 2, )
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> [1] 2

If you really need to translate empty arguments as NULL there is no other choice than quoting the dots and evaluating them after you've converted the missing arguments. Now if you do that with eval(substitute(alist(...)) you will get into trouble if you attempt to forward dots through several function calls. Base R provides no way of evaluating the quoted argument in their original environment.

Luckily it's easy to do by capturing the dots in quosures:

library("purrr")  # For convenient map_if()

my_dots <- function(...) {
  quos <- rlang::quos(..., .ignore_empty = "none")
  quos <- map_if(quos, rlang::quo_is_missing, function(x) NULL)
  map(quos, rlang::eval_tidy)
}

my_dots(1, , 2, )
#> [[1]]
#> [1] 1
#>
#> [[2]]
#> NULL
#>
#> [[3]]
#> [1] 2
#>
#> [[4]]
#> NULL
like image 96
Lionel Henry Avatar answered Jan 04 '23 22:01

Lionel Henry