When can mapply's MoreArgs argument not be replaced by R's vector recycling rules?

Viewed 274

I'm trying to come up with an example of when mapply's MoreArgs argument is useful. I have been utterly defeated. Outrageously, even the example given in mapply's documentation is inadequate. The docs give mapply(rep, times = 1:4, MoreArgs = list(x = 42)) as their only example of the use of MoreArgs, but I have found that R's vector recycling rules mean that mapply(rep, times = 1:4, 42) gives exactly the same output.

So when can MoreArgs not be replaced by just deleting the corresponding MoreArgs = list(...) wrapper? I tried to come up with some examples myself, but I failed every time. For example, mapply(rnorm,1:10,11:20,MoreArgs = list(5),SIMPLIFY = FALSE) is identical to mapply(rnorm,1:10,11:20,5,SIMPLIFY = FALSE).

Following the lead of the comments, I've also tried examples where we need to recycle a vector for each call to the function that mapply is being called on. However, it seems that mapply has no difference in functionality between using a list containing said vector - e.g. list(c(0.6,0.3,0.1)) - as a ... argument or as a MoreArgs argument. In the MoreArgs case, the content of the list is recycled as expected and in the ... case, the vector recycling rules mean that the content of the list will be recycled in each case, giving identical functionality to MoreArgs. This brings me back to the original question: When can the MoreArgs argument of mapply be used to give functionality that cannot be gotten from the ... argument and the vector recycling rules? I've yet to see a case where identical functionality cannot be gained by simply deleting the MoreArgs= argument and letting the relevant parts pass to ....

3 Answers

If we are passing a named argument, MoreArgs can be passed in a list. Sometimes, the argument depends on the order of arguments in the function and it is evaluated in that order. e.g. if we want the specify argument n

mapply(rnorm, 1:10, 11:20, MoreArgs = list(n = 5),SIMPLIFY = FALSE)

It works even without the MoreArgs as well because it does the recycling for each element being passed. However, passing arguments in MoreArgs can help in distinguishing the function arguments from the inputs to the mapply/Map. These are mostly used when we invoke functions without a lambda/anonymous function as it can also be written as

mapply(function(x, y) rnorm(n = 5, mean = x, sd = y), 1:10, 11:20, SIMPLIFY = FALSE)

Note that the below one fails because there are no inputs to mapply, but only function argument input

mapply(rnorm, MoreArgs = list(n = 5, mean = 1:10, sd = 11:20))

When we pass the arguments as in the order specified in the OP's post without a name, assuming the n is 5, it is not taken as 5, instead it is now the sd because the order of rnorm is n, mean and sd as showed in the Usage of ?rnorm

rnorm(n, mean = 0, sd = 1)

mapply(rnorm, 1:10, 11:20, 5, SIMPLIFY = FALSE)

compare this to

mapply(rnorm, 5, 1:10, 11:20, SIMPLIFY = FALSE)

or

mapply(rnorm, 1:10, 11:20, n = 5, SIMPLIFY = FALSE)

which can be otherwise written in MoreArgs as well as showed in the first syntax

Update: I have come to believe that this answer is incorrect. My embolden concern below appears correct, voiding a key part of this answer. For a proper answer, see my other answer.

Following Henrik's lead from the comments, we can give an answer. MoreArgs is best used for the arguments that you want to recycle, but not in the way that Mapply wants to. This is typically the case when said argument is a vector or list. What follows is an extended example.

Consider the rmultinom function. It's very badly behaved. It takes 3 arguments: n controlling how many samples to take, size controlling how big each sample is, and the vector of probabilities prob, where each probability in the vector can be seen as the chance that a biased die of length(prob) sides rolls the face corresponding to the entry in prob in question. Its output is a vector showing the number of times that the biased die landed on each side. For example, this is one possible input and output (read from top to bottom, not left to right):

 rmultinom(3,10,c(0.6,0.3,0.1))
     [,1] [,2] [,3]
[1,]    6    6    6
[2,]    2    2    4
[3,]    2    2    0

Why do I say that rmultinom is badly behaved? Because this is also valid:

rmultinom(3,10,c(0.6))
     [,1] [,2] [,3]
[1,]   10   10   10

You'd think that giving rmultinom a vector of probabilities that don't sum to 1 would throw an error, but for technical reasons that I don't claim to understand, it doesn't.

So let's get back to mapply. Suppose that I want to test with n as 3, 4, and 5 with a sample of size 10 for the first test, 11 for the second test, and 12 for the last. I want prob to stay fixed at c(0.6,0.3,0.1). If I get that last bit wrong, then we'll get the bad behavior that we had earlier.

Let's start by trying mapply(rmultinom,3:5,10:12,c(0.6,0.3,0.1)):

mapply(rmultinom,3:5,10:12,c(0.6,0.3,0.1))
[[1]]
     [,1] [,2] [,3]
[1,]   10   10   10

[[2]]
     [,1] [,2] [,3] [,4]
[1,]   11   11   11   11

[[3]]
     [,1] [,2] [,3] [,4] [,5]
[1,]   12   12   12   12   12

That didn't work. I can tell you that using list(0.6,0.3,0.1) as our last argument doesn't work either. It gives exactly the same output, so I won't even shown it. How about forgetting about c or list and just using 0.6,0.3,0.1 directly?

mapply(rmultinom,3:5,10:12,0.6,0.3,0.1)
Error in (function (n, size, prob)  : 
  unused arguments (dots[[4]][[1]], dots[[5]][[1]])

No luck there. Because of the way that rmultinom works, I can't tell if the vector recycling rule or mapply have betrayed us, but I cannot think of any sensible ways to use the recycling rule that we have not already tried. So how about using MoreArgs with our original vector?

Error in mapply(rmultinom, 3:5, 10:12, MoreArgs = c(0.6, 0.3, 0.1)) : 
  argument 'MoreArgs' of 'mapply' is not a list

Alright then, we'll have to use a list...

mapply(rmultinom,3:5,10:12,MoreArgs=list(0.6,0.3,0.1))
Error in (function (n, size, prob)  : unused arguments (0.3, 0.1)

A list of our vector then!

> mapply(rmultinom,3:5,10:12,MoreArgs=list(c(0.6,0.3,0.1)))
[[1]]
     [,1] [,2] [,3]
[1,]    4    8    9
[2,]    3    1    0
[3,]    3    1    1

[[2]]
     [,1] [,2] [,3] [,4]
[1,]    6    8    4    7
[2,]    4    3    7    3
[3,]    1    0    0    1

[[3]]
     [,1] [,2] [,3] [,4] [,5]
[1,]    7    6    9    8    7
[2,]    2    2    3    3    4
[3,]    3    4    0    1    1

And now we finally have something that works. Incidentally, mapply(rmultinom,3:5,10:12,list(c(0.6,0.3,0.1))) also works, but I think that's because mapply knows to pass list(c(0.6,0.3,0.1)) as MoreArgs=list(c(0.6,0.3,0.1)). I'd be interested to know if anyone can confirm that. It could be the case that mapply is recycling the first entry of list(c(0.6,0.3,0.1)), which would be the vector c(0.6,0.3,0.1), rendering this entire answer false.

Assuming that the above isn't a case of recycling, then we can confidently say that we couldn't find a vector recycling way to do what we wanted to, which therefore answers the question and shows that we needed MoreArgs. But as a bonus, it's worth showing how to do this with anonymous functions:

mapply(function(x,y,z) rmultinom(x,y,c(0.6,0.3,0.1)),3:5,10:12)
[[1]]
     [,1] [,2] [,3]
[1,]    5    7    4
[2,]    4    2    5
[3,]    1    1    1

[[2]]
     [,1] [,2] [,3] [,4]
[1,]    5    7    5    5
[2,]    5    1    5    4
[3,]    1    3    1    2

[[3]]
     [,1] [,2] [,3] [,4] [,5]
[1,]   10    8    8    8    8
[2,]    2    2    3    3    3
[3,]    0    2    1    1    1

Personally, the anonymous functions way to do it seems a lot more natural, but I suppose that it might get a bit ugly if you have a few too many arguments. mapply(function(...,z) rmultinom(...,c(0.6,0.3,0.1)),3:5,10:12) and mapply(function(...) rmultinom(...,c(0.6,0.3,0.1)),3:5,10:12) also work, but I have no idea if they're safe or idiomatic.

I've yet to see a case where identical functionality cannot be gained by simply deleting the MoreArgs= argument and letting the relevant parts pass to ....

This is wrong, but only slightly. Compare:

options(max.print = 50)#Before running this, make sure that you know how to undo it.

> mapply(sum,1:5,MoreArgs=list(runif(10),runif(10000)))
[1] 5019.831 5020.831 5021.831 5022.831 5023.831

> mapply(sum,1:5,list(runif(10)),list(runif(10000)))
[1] 5069.321 5070.321 5071.321 5072.321 5073.321

> mapply(sum,1:5,list(runif(10),runif(10000)))
[1]    6.658275 4984.177882    8.658275 4986.177882   10.658275

> mapply(sum,1:5,runif(10),runif(10000))
 [1] 1.750417 3.286090 3.186474 5.310268 5.962829 1.343564 2.325567 3.928796 4.955376
[10] 5.507385 1.992290 3.454536 3.399763 5.242883 5.589296 1.637056 2.964259 3.839006
[19] 5.647123 5.883139 1.863512 2.827110 3.633137 5.174900 5.365155 2.022725 3.139846
[28] 3.830624 5.064546 5.697612 1.242803 3.456888 3.726114 5.271773 5.881724 1.533730
[37] 2.489976 3.509690 5.657166 5.400823 1.972689 2.858276 3.571505 5.582752 5.482381
[46] 1.956237 2.497409 3.864434 5.389969 5.965341
 [ reached getOption("max.print") -- omitted 9950 entries ]
Warning message:
In mapply(sum, 1:5, list(runif(10), runif(10000))) :
  longer argument not a multiple of length of shorter

In the first case, every element of the list in the MoreArgs argument is recycled for each call. Similarly, the second case is recycling both runif(10) and runif(10000) for each call, giving behavior that I'm confident to call identical. The fourth case only exists to shows what we get if we're silly enough to not use any lists at all.

My above quoted claim is that the first and third cases should be identical. This is clearly not the case. If we try to use one list (rather than two, as our second case did) without MoreArgs, R's normal vector recycling rules will have us reuse the value of runif(10) for the first, third, and fifth calls, and use runif(10000) for the second and fourth, and give us a warning due to this odd behavior.

In conclusion, it still appears that the MoreArgs argument can always be replaced by R's vector recycling rules (despite my previous answer), but not in the exact way that I said in the question. The truth appears to be that MoreArgs=list(foo,bar,etc) is equivalent to using list(foo), list(bar), and list(etc) as ... arguments to mapply. Notice that this is not the same as using list(foo,bar,etc) as a ... argument. So, ultimately: You will not always get identical functionality from omitting the MoreArgs argument.

As a final minor detail: Omitting the MoreArgs argument is harmless, but omitting the ... argument and using MoreArgs instead gives unexpected output, usually an empty list.

Related