0

Many questions have been asked about how to use ... in R. However, I have not seen a good explanation of how it works under the hood. How is ... implemented? I'm just looking for a high level overview.

The ... feature is highly useful, but it seems to be a black box for a lot of people. I think understanding what makes it work would go a long way towards demystifying the concept.

lead-y
  • 46
  • 4
  • This is my first question on Stack Overflow. If you think it needs improvement, please give me feedback and I'll edit the question! – lead-y Apr 23 '21 at 14:48
  • 1
    A strongly related question is [How to see the source code of R .Internal or .Primitive function?](https://stackoverflow.com/q/14035506/903061), though it's not a dupe because it's certainly nontrivial to go from those answers to finding where in the parser code `...` is managed. – Gregor Thomas Apr 23 '21 at 15:00
  • 1
    @GregorThomas thanks. I'm think a high level overview could still be helpful, even if it doesn't go into the nitty-gritty of the internal R C code. – lead-y Apr 23 '21 at 15:10

1 Answers1

2

When a function is called, R goes through a process of matching the arguments the caller used to formal arguments for the function. The match.call function in R duplicates this process. So for example,

f <- function(x = 1, y, ...) {
  match.call(expand.dots = FALSE)
}

f(2)
#> f(x = 2)
f(y = 3)
#> f(y = 3)
f(z = 4)
#> f(... = pairlist(z = 4))

Created on 2021-04-23 by the reprex package (v1.0.0)

You can see that z, which is not a formal argument, gets caught up in the ... argument, which is internally a "pairlist". You can use the ... argument in other calls; if f() had

g(...)

in its body, then f(z = 4) would end up calling g(z = 4), because the pairlist of arguments would be substituted for the ... in the call. This is commonly used in the form list(...) to turn ... into a regular vector list: it calls the list() function with arguments from the pairlist.

There are a few special constructions for working with ...: see the ?"..." help page for details.

user2554330
  • 37,248
  • 4
  • 43
  • 90
  • So ... takes all caller-defined arguments that are not formal arguments, and captures them as a pairlist? Thank you, I think the concept of a pairlist is the key here. – lead-y Apr 23 '21 at 15:14
  • 1
    Pairlists act very much like regular lists, but the implementation is different: pairlists are linked lists (so adding and removing entries is really quick), while regular lists are vectors (so accessing entries by index is quick). I imagine the call starts as a pairlist, and args are pulled out of it as they are matched to formal arguments; the `...` pairlist is what is left at the end. – user2554330 Apr 23 '21 at 16:17
  • 1
    Partial match of argument name can often lead to strange behavior :-) see for ex `test=function(xx=1,...) { print(xx); print(list(...)) }` which when called with `test(x=3)` will print `3` and `list()` instead of `1` and `list(c=3)` as expected ! – Billy34 Apr 23 '21 at 16:47
  • Your expectations don't match the documented behaviour. Yes, it's strange, but it shouldn't be a surprise. – user2554330 Apr 23 '21 at 21:53