2

I thought I had understood how to use the additional arguments argument (...) of purrr::map. Here is some code that hopefully illustrates the (to me) unexpected behaviour of purrr::map:

It seems that passing argument a as additional argument in purrr::map is not working:

library(purrr)

f <- function(a, b) {
  a + b
}

g <- function(a = 0, b) {
  a + b
}

map(1:3, .f = ~ f(b = .x, a = 1))
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 3
#> 
#> [[3]]
#> [1] 4
map(1:3, .f = ~ f(b = .x), a = 1)
#> Error in f(b = .x): argument "a" is missing, with no default

map(1:3, .f = ~ g(b = .x, a = 1))
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 3
#> 
#> [[3]]
#> [1] 4
map(1:3, .f = ~ g(b = .x), a = 1)
#> [[1]]
#> [1] 1
#> 
#> [[2]]
#> [1] 2
#> 
#> [[3]]
#> [1] 3

lapply(1:3, function(b, a = 1) f(a, b))
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 3
#> 
#> [[3]]
#> [1] 4
lapply(1:3, function(b, a) f(a, b), a = 1)
#> [[1]]
#> [1] 2
#> 
#> [[2]]
#> [1] 3
#> 
#> [[3]]
#> [1] 4

My question is why does the code:

map(1:3, .f = ~ f(b = .x), a = 1)

throw an error?

Ramiro Magno
  • 3,085
  • 15
  • 30
  • You could anonymous function as in `lappy` `map(1:3, .f = function(b, a = 1) f(a, b))` – akrun Aug 01 '20 at 16:09
  • You didn't mentioned that your functions were changeable – akrun Aug 01 '20 at 17:41
  • 1
    @akrun I don't think that duplicate is correct. The question here is trying to understand *why* `a=1` outside the lambda definition is throwing an error. The question you linked is focused on *how* to forward the argument, but doesn't really explain the "why". – Artem Sokolov Aug 01 '20 at 17:55

2 Answers2

2

We could pass the remaining arguments without any anonymous function

library(purrr)
map(1:3, f, a = 1)
#[[1]]
#[1] 2

#[[2]]
#[1] 3

#[[3]]
#[1] 4

Or another option is rlang::as_function or purrr:as_mapper

map(1:3, as_mapper(f), a = 1)

Or create the f on the fly

map(1:3, as_mapper(~ .x + .y), a = 1)

Or call it in invoke

map(1:3, ~ invoke(f, b = .x, a = 1))
#[[1]]
#[1] 2

#[[2]]
#[1] 3

#[[3]]
#[1] 4

This would make it more easier to read than the .f = ~ f(b = .x), a = 1

akrun
  • 874,273
  • 37
  • 540
  • 662
  • Thanks @akrun!. With your code example it seems then that the additional arguments are passed onto the mapped function first, and only then the to be iterated arguments are matched by position with whatever arguments are left. Is it? – Ramiro Magno Aug 01 '20 at 16:16
  • I appreciate your solutions but the examples are only for illustration. My question is more geared towards the understanding of `map`'s functionality as per the documentation. According to the docs I thought I could pass extra arguments (e.g., `a`) to the mapped function, regardless of it being a mapper or not. – Ramiro Magno Aug 01 '20 at 16:23
  • Thanks, I clarified now what my issue really is. – Ramiro Magno Aug 01 '20 at 16:27
  • 1
    @rmagno Not sure what would be the benefit in placing it outside the function instead of `map(1:3, .f = ~ f(a = 1, b = .x))` or without having any anonymous call. The one you mentioned would make it even difficult to understand from a code POV. You pass a function leaving out the `a` argument, then you are passing `a` separately outside the function invoke call – akrun Aug 01 '20 at 16:29
  • So you're saying then that the `...` of `map` are useless because we can always passed those arguments inline with the anonymous function definition? – Ramiro Magno Aug 01 '20 at 16:34
2

Behind the scenes, map() calls as_mapper(). We can do this by hand to see what's going on:

purrr::as_mapper( ~ f(b = .x, a = 1) )
# <lambda>
# function (..., .x = ..1, .y = ..2, . = ..1) 
# f(b = .x, a = 1)                                <----
# attr(,"class")
# [1] "rlang_lambda_function" "function"


purrr::as_mapper( ~ f(b = .x), a=1 )
# <lambda>
# function (..., .x = ..1, .y = ..2, . = ..1) 
# f(b = .x)                                       <----
# attr(,"class")
# [1] "rlang_lambda_function" "function"           

I highlighted the important distinction with <---. Notice that in the second case, the lambda function that gets created does not incorporate your extra a=1 parameter, which leads to the error you are observing.

To address your comment, a=1 actually is being passed to the lambda function. Your lambda function just isn't doing anything with it. To properly incorporate a, the lambda function definition needs to handle the ... dots:

g <- function(a, b, ...) {a + b}               # ... are needed to catch all extra 
                                               #   arguments from as_mapper

purrr::as_mapper( .f = ~ g(b=.x, ...) )
# <lambda>
# function (..., .x = ..1, .y = ..2, . = ..1) 
# g(b = .x, ...)                               <-- dots are now forwarded to g()
# attr(,"class")
# [1] "rlang_lambda_function" "function"            

purrr::map(1:3, .f = ~ g(b=.x, ...), a=1 )     # a now properly gets passed to g
# [[1]]
# [1] 2
#
# [[2]]
# [1] 3
#
# [[3]]
# [1] 4
Artem Sokolov
  • 13,196
  • 4
  • 43
  • 74
  • Thank you @artem-sokolov. That indeed helps understanding why `a = 1` is not being passed on to `f`. But then I think `map` help page is misleading when it says that: `...` *Additional arguments passed on to the mapped function.*. Because this only happens if `.f` is a function object. If it is a formula object then the additional arguments are not incorporated. Would you agree? – Ramiro Magno Aug 01 '20 at 16:50
  • 1
    @rmagno It's a bit more subtle than that. The additional argument `a=1` **is** being passed to the lambda function. It's just that your definition of the function is not doing anything with that extra argument (i.e., it's not forwarding the argument to `f`). Please see my edit that shows how to explicitly forward the extra argument from map() to the custom function in the lambda definition. – Artem Sokolov Aug 01 '20 at 17:09