4

I'm struggling with a problem that is acknowledged but for which I have yet to find an easy solution: how to catch misspecified arguments passed through to R functions, when the function definition includes the ellipsis or three dots ....

As stated in Wickham's Advanced R:

Using ... comes at a price — any misspelled arguments will not raise an error, and any arguments after ... must be fully named. This makes it easy for typos to go unnoticed.

Here's an example:

myfun <- function(x, ...) 
    UseMethod("myfun")

myfun.character <- function(x, toLower = FALSE, ...)
    cat("Sent and transformed by myfun:", ifelse(toLower, tolower(x), x), "\n")

myfun("Test String with CaPiTaLs.")
## Sent and transformed by myfun: Test String with CaPiTaLs.
myfun("Test String with CaPiTaLs.", toLower = TRUE)
## Sent and transformed by myfun: test string with capitals. 
myfun("Test String with CaPiTaLs.", tolower = TRUE)
## Sent and transformed by myfun: Test String with CaPiTaLs.
myfun("Test String with CaPiTaLs.", toLower = TRUE, notDefined = 1)
## Sent and transformed by myfun: test string with capitals. 

Here, the subtle misspelling of toLower causes no visible error except the failure of the output to be lower case, as the user probably expected. In the last example, arguments that the user thought would be functional are not, but they simply disappear into the ether because of the nature of ....

Of course I could inspect the contents of ... through list(...), but I am wondering if there is a better way, linked to the formals defined in the function or those to which ... is passed.

And by the way, does anyone know what we should be calling the ...? I am nearly sure I saw a reference with a name in the Julia docs but cannot find this now!

Added:

Ideally I'd like to figure out the best way to generate an error if an argument is not something that ought to be passed through in the ellipsis, when something in the scope of the called function passed through the ellipsis to a subsidiary argument. For instance, how best to trap these errors:

myfun2 <- function(x, ...) 
    UseMethod("my fun")

myfun2.character <- function(x, toLower = FALSE, ...)
    cat("Sent and transformed by myfun:", ifelse(toLower, tolower(x), x), "\n", ...)

myfun2("Test String with CaPiTaLs.", tolower = TRUE)
## Sent and transformed by myfun: Test String with CaPiTaLs. 
##  TRUE
myfun2("Test String with CaPiTaLs.", toLower = TRUE, notDefined = 1)
## Sent and transformed by myfun: test string with capitals. 
##  1
myfun2("Test String with CaPiTaLs.", sep = "\tXX\t")
## Sent and transformed by myfun:   XX  Test String with CaPiTaLs.  XX  
Ken Benoit
  • 14,454
  • 27
  • 50
  • Do you want to use the `...` in your `myfun.character`? If not, the check could simply be `if (length(list(...))) stop(...)`. – Jan van der Laan Nov 13 '15 at 13:30
  • It's called ellipsis. I see two options in this example: don't provide a default for `toLower` or include alternative spellings as parameters, e.g., `toLower = tolower`. – Roland Nov 13 '15 at 13:31
  • This is a very simplified example, but I'm thinking of how to manage this in a package with longer function signatures and lots of named formals. I'm concerned that if I try to set up manual checks on the `list(...)` contents then I will increase the possibility of errors when my checks diverge from the function signatures. – Ken Benoit Nov 13 '15 at 13:33
  • I thought that perhaps S4 might help, but it doesn't. – Jan van der Laan Nov 13 '15 at 13:52
  • Can you please clarify what you'd like to achieve? What should `myfun("Test String with CaPiTaLs.", tolower = TRUE)` return and why should it return that? Right now your question reads like you simultaneously want to use `...` and not use it. – Roland Nov 13 '15 at 14:17
  • Yes, I've edited the question to try to make this more clear. I'm trying to use `...` but trap things that don't belong there as well. This needs to work for generics that I define as well as pre-existing generics for which I define additional methods (and hence must use `...`). – Ken Benoit Nov 13 '15 at 16:13

1 Answers1

2

I would not return an error. A warning should be sufficient. Something like this:

myfun2.character <- function(x, toLower = FALSE, ...) {
  elli <- names(list(...))
  check <- elli %in% names(formals(cat))
  if (any(!check)) warning(sprintf("%s is not an expected parameter. Did you misstype it?", 
                                   paste(elli[!check], collapse = ", ")))
  cat("Sent and transformed by myfun:", ifelse(toLower, tolower(x), x), "\n", ...)
}


myfun2("Test String with CaPiTaLs.", tolower = TRUE)
#Sent and transformed by myfun: Test String with CaPiTaLs. 
#TRUE
#Warning message:
#  In myfun2.character("Test String with CaPiTaLs.", tolower = TRUE) :
#  tolower is not an expected parameter. Did you misstype it?
myfun2("Test String with CaPiTaLs.", toLower = TRUE, notDefined = 1)
#Sent and transformed by myfun: test string with capitals. 
#1
#Warning message:
#  In myfun2.character("Test String with CaPiTaLs.", toLower = TRUE,  :
#                        notDefined is not an expected parameter. Did you misstype it?
myfun2("Test String with CaPiTaLs.", sep = "\tXX\t")
## Sent and transformed by myfun:   XX  Test String with CaPiTaLs.  XX

I don't think the last one should warn. You should avoid positional argument matching anyway and if you use name matching there, you'd get an error, which you'd need to catch.

Roland
  • 127,288
  • 10
  • 191
  • 288
  • Makes perfect sense, thanks. Found this discussion too: http://stackoverflow.com/questions/25376197/split-up-arguments-and-distribute-to-multiple-functions. Some good alternatives there as well. – Ken Benoit Nov 13 '15 at 21:32