2

As I understand, normally you don't need to quote or unquote dots when they are not being modified (e.g. by changing their names). This example makes it seem like I don't really understand how that works, however.

Here we have a function that relies on dots to select columns for nesting. All it does is add a column from the foo argument, and then nest all columns not mentioned in the dots.

library(tidyverse)
dots_fun <- function(df, foo, ...) {
  df %>%
    mutate(foo = foo) %>%
    nest(data = -c(...))
}

dots_fun(mtcars, "a", cyl)
#> # A tibble: 3 x 2
#>     cyl data              
#>   <dbl> <list>            
#> 1     6 <tibble [7 × 11]> 
#> 2     4 <tibble [11 × 11]>
#> 3     8 <tibble [14 × 11]>

I want to be able to map this function, by calling it with different arguments. A naive approach to doing this by using a normal anonymous function syntax fails with a confusing error:

list_of_foos <- c("a", "b")

mapping_fun1 <- function(df, foos, ...) {
  map(
    .x = foos,
    .f = ~ dots_fun(df = df, foo = .x, ...)
  )
}

mapping_fun1(mtcars, foos = list_of_foos, cyl)
#> Error: Can't subset columns that don't exist.
#> x The column `a` doesn't exist.

Nor does it help if I just move the dots outside of the anonymous function. It no longer errors, but fails to nest on cyl as desired.

mapping_fun2 <- function(df, foos, ...) {
  map(
    .x = foos,
    .f = ~ dots_fun(df = df, foo = .x),
    ...
  )
}
mapping_fun2(mtcars, foos = list_of_foos, cyl)
#> [[1]]
#> # A tibble: 1 x 1
#>   data              
#>   <list>            
#> 1 <tibble [32 × 12]>
#> 
#> [[2]]
#> # A tibble: 1 x 1
#>   data              
#>   <list>            
#> 1 <tibble [32 × 12]>

I managed to get it to work by splicing the dots into the anonymous function, but I don't really understand why this was necessary. (You can also get it to work by reversing the order of the arguments of the mapped function and supplying all arguments through the ... of map, but then dots_fun has the "wrong" argument order. It doesn't work if you use a function() style anonymous function to reverse the argument order)

mapping_fun3 <- function(df, foos, ...) {
  dots <- enquos(...)
  map(
    .x = foos,
    .f = ~ dots_fun(df = df, foo = .x, !!!dots)
  )
}

mapping_fun3(mtcars, foos = list_of_foos, cyl)
#> [[1]]
#> # A tibble: 3 x 2
#>     cyl data              
#>   <dbl> <list>            
#> 1     6 <tibble [7 × 11]> 
#> 2     4 <tibble [11 × 11]>
#> 3     8 <tibble [14 × 11]>
#> 
#> [[2]]
#> # A tibble: 3 x 2
#>     cyl data              
#>   <dbl> <list>            
#> 1     6 <tibble [7 × 11]> 
#> 2     4 <tibble [11 × 11]>
#> 3     8 <tibble [14 × 11]>

My question is: in what condition/situation do you need to quote and unquote ... to pass them safely through functions? and how does that condition apply here?

Calum You
  • 14,687
  • 4
  • 23
  • 42
  • 1
    What version of `tidyr` are you using? I get errors when calling `dots_fun(mtcars, "a", cyl)` (`cyl` not found). I used `tidyr_1.0.2` – MrFlick Mar 04 '20 at 01:34
  • I think the problem is related to the fact you need to pass the ellipses though both the `map` and inner function: https://stackoverflow.com/questions/48215325/passing-ellipsis-arguments-to-map-function-purrr-package-r but i'm having a hard time replicating. – MrFlick Mar 04 '20 at 01:50

1 Answers1

3

I think your problem is that you need to pass the ... though each level of function call. So the ... both have to pass through map() as well as your inner function.

I could not get your example to work with nest(), so I made a version that uses select() instead

dots_fun <- function(df, foo, ...) {
  df %>%
    mutate(foo = foo) %>%
    select(...)
}

And then, it doesn't seem you can actually use the as_mapper syntax with ... and non-standard evaluation via this github issue so you need to explicitly create an anonymous function so the iterated value isn't passed a second time in the ... values as well. Hadley said the ~ syntax is only for "simple" functions and not those with .... So a working mapping function might look like this

mapping_fun1 <- function(df, foos, ...) {
  map(
    .x = foos,
    .f = function(x, ...) dots_fun(df = df, foo = x, ...),
    ...
  )
}
mapping_fun1(mtcars, foos = list_of_foos, cyl, gear)

We pass the ... though map(), through our anonymous function, and finally into dots_fun. If you break that chain at any point, it falls apart.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • Nice answer. The technical reason that `...` doesn't work this way in lambda-formulas is that they use the dots for another purpose, so you can refer to arguments as `..1`, `..2` etc. – Lionel Henry Mar 04 '20 at 08:40
  • Thanks for clearing that up! It makes a lot more sense now – Calum You Mar 04 '20 at 18:36