Functional programming with either *apply
or purrr
is my bread and butter yet I do not understand how pmap
handles function arguments.
I've been looking through multiple similar questions yet fell short to find the appropriate answer to
A) what is going on and
B) how can I use arbitrary function argument names?
# dummy data -----------------------------------------------------------------
(iter_tibble <- tibble::tibble(a = 1:2,
b = 3:4,
c = 7:6))
#> # A tibble: 2 x 3
#> a b c
#> <int> <int> <int>
#> 1 1 3 7
#> 2 2 4 6
# pmap it --------------------------------------------------------------------
# standard way
purrr::pmap(iter_tibble, function(a, b, c) {
paste(a, b, c)
})
#> [[1]]
#> [1] "1 3 7"
#>
#> [[2]]
#> [1] "2 4 6"
# switch order
# works and a maps to a, b to b etc
purrr::pmap(iter_tibble, function(b, c, a) {
paste(a, b, c)
})
#> [[1]]
#> [1] "1 3 7"
#>
#> [[2]]
#> [1] "2 4 6"
# name arguments
purrr::pmap(iter_tibble, function(a1 = a, b1 = b, c1 = c) {
paste(a1, b1, c1)
})
#> [[1]]
#> [1] "1 3 7"
#>
#> [[2]]
#> [1] "2 4 6"
# name arguments and switch order
purrr::pmap(iter_tibble, function(b1 = b, c1 = c, asterix = a) {
paste(b1, asterix, c1)
})
#> [[1]]
#> [1] "3 1 7"
#>
#> [[2]]
#> [1] "4 2 6"
# but when using a different initial letter
# ERROR
purrr::pmap(iter_tibble,
purrr::safely(
function(b1 = b, c1 = c, obelix = a) {
paste(b1, obelix, c1)
}
))[1]
#> [[1]]
#> [[1]]$result
#> NULL
#>
#> [[1]]$error
#> <simpleError in .f(...): unused argument (a = 1)>
This behavior is rather different from how arguments can be called in regular R functions where abbreviations are possible (yet bad practice) but extensions are not possible.
# regular function usage -----------------------------------------------------
# abbrevate arguments - no problem
sample(1:4, s = 5, repla = TRUE)
#> [1] 1 3 4 3 1
# extend arguments? nope
sample(1:4, size = 5, replaceeeee = TRUE)
#> Error in sample(1:4, size = 5, replaceeeee = TRUE): unused argument (replaceeeee = TRUE)
My guess Is that the answer is rather about the pmap
call to C than what happens in R.
The misunderstanding here is that your 3rd and 4th options do not have "named arguments" but default argument values. You are supplying a function definition to the .f
argument of pmap
, not a function call.
pmap
is doing partial argument matching the same way that base R does. It may make this clearer to turn on options(warnPartialMatchArgs = TRUE)
. Here I'll take your 3rd example, factoring the function definition out to make it clearer what is happening:
iter_tibble <- tibble::tibble(
a = 1:2,
b = 3:4,
c = 7:6
)
f3 <- function(a1 = a, b1 = b, c1 = c) {
paste(a1, b1, c1)
}
purrr::pmap(iter_tibble, f3)
#> Warning in .f(a = .l[[1L]][[i]], b = .l[[2L]][[i]], c = .l[[3L]][[i]], ...):
#> partial argument match of 'a' to 'a1'
#> Warning in .f(a = .l[[1L]][[i]], b = .l[[2L]][[i]], c = .l[[3L]][[i]], ...):
#> partial argument match of 'b' to 'b1'
#> Warning in .f(a = .l[[1L]][[i]], b = .l[[2L]][[i]], c = .l[[3L]][[i]], ...):
#> partial argument match of 'c' to 'c1'
#> Warning in .f(a = .l[[1L]][[i]], b = .l[[2L]][[i]], c = .l[[3L]][[i]], ...):
#> partial argument match of 'a' to 'a1'
#> Warning in .f(a = .l[[1L]][[i]], b = .l[[2L]][[i]], c = .l[[3L]][[i]], ...):
#> partial argument match of 'b' to 'b1'
#> Warning in .f(a = .l[[1L]][[i]], b = .l[[2L]][[i]], c = .l[[3L]][[i]], ...):
#> partial argument match of 'c' to 'c1'
#> [[1]]
#> [1] "1 3 7"
#>
#> [[2]]
#> [1] "2 4 6"
This is exactly the same as the case with regular R functions that you describe, where supplied named arguments can be abbreviations of the function arguments. To put it another way, for the first row of the table, pmap
basically constructs the call f3(a = 1, b = 3, c = 7)
. a
, b
, and c
come from the column names, the values come from the row.
When trying to evaluate this call, we see that the function f3
does not have an argument a
, but it has an argument a1
. So the named argument a = 1
in the call is partially matched to a1
in the function definition. That's what the partial match warnings describe in the output. No "extensions" are happening. The fact that the argument a1
has a default value of a
is totally irrelevant here.
If you want to call a function and pass values in the tibble to differently named arguments, use a wrapper around it to change the interface. You can do this with a separate named function or (as is very common) using the ~
anonymous function syntax. Using your 5th example:
iter_tibble <- tibble::tibble(
a = 1:2,
b = 3:4,
c = 7:6
)
f5 <- function(b1, obelix, c1) {
paste(b1, obelix, c1)
}
f5_wrapper <- function(a, b, c) {
f5(b1 = b, obelix = a, c1 = c)
}
purrr::pmap(iter_tibble, f5_wrapper)
#> [[1]]
#> [1] "3 1 7"
#>
#> [[2]]
#> [1] "4 2 6"
purrr::pmap(iter_tibble, ~ f5(b1 = ..2, obelix = ..1, c1 = ..3))
#> [[1]]
#> [1] "3 1 7"
#>
#> [[2]]
#> [1] "4 2 6"