Calum You
Calum You

Reputation: 15072

Why do I need to quote-unquote dots when passing them to a mapped function in purrr?

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?

Upvotes: 2

Views: 214

Answers (1)

MrFlick
MrFlick

Reputation: 206253

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.

Upvotes: 3

Related Questions