tidyverse / purrr

A functional programming toolkit for R
https://purrr.tidyverse.org/
Other
1.28k stars 272 forks source link

pmap with .f argument in function format generates strange errors. #983

Closed siavash-babaei closed 2 years ago

siavash-babaei commented 2 years ago

Problem:

  1. In certain cases, running purrr::pmap with .f argument in function format generates strange error.
  2. There is no problem when using the formula format for .f argument.

When you have named list of lists for the .l argument and are using an anonymous function for the .f argument, you need to use the list names as arguments, e.g., if .l = list(args_list_1 = ..., args_list_2 = ...) then we must use function(arg_list_1, arg_list_2) { ... } rather than say function(arg_1, arg_2) { ... } which is what you could do in case you were utilising an un-named list of lists for .l argument .

Kind of strange and unnecessary behaviour in my opinion. Comparing this R code with similar OCAML/F#: Pattern matching in R is definitely awkward or maybe ML languages somewhat spoil a person in such regards.

Solution 1: Use names of lists from within the .l lists of lists.

Solution 2

  1. Use base lists rather than tibble lists to feed the .l argument.
  2. Remove any naming beyond the actual arguments lists.
System     : Windows 10 x64                   (latest updates)
R Ver.     : "R version 4.0.2 (2020-06-22)"
Repo       : "https://mran.microsoft.com/"    (latest daily snapshot of CRAN)
purrr Ver. : "0.3.5"                          (latest tidyverse updates)
library(purrr)

rnorm_args <- list(n = 2, mean = 0, sd  = 1)
runif_args <- list(n = 2, min  = 0, max = 1)
rand_arg   <- list(rnorm_args = rnorm_args, runif_args = runif_args)
rand_fn    <- list(rnorm_fn = rnorm, runif_fn = runif)
input      <- list(rand_fn = rand_fn, rand_arg = rand_arg)

# .f argument in formula format :  Works as expected
pmap(.l = input,
     .f = ~ exec(.fn = .x, !!!.y))
#> $rnorm_fn
#> [1] -3.4087219 -0.7465965
#> 
#> $runif_fn
#> [1] 0.8398340 0.2766491

# .f argument in function format: Generates Error
pmap(.l = input,
     .f = function(fn, args) exec(.fn = fn, !!!args))
#> Error in .f(rand_fn = .l[[1L]][[i]], rand_arg = .l[[2L]][[i]], ...): unused arguments (rand_fn = .l[[1]][[i]], rand_arg = .l[[2]][[i]])

# FIX 1: Use names of lists in input list
# .f argument in function format : NOW, It works as expected ..
pmap(.l = input,
     .f = function(rand_fn, rand_arg) exec(.fn = rand_fn, !!!rand_arg))
#> $rnorm_fn
#> [1]  0.4274880 -0.4521357
#> 
#> $runif_fn
#> [1] 0.3050558 0.8541712

# FIX 2: Remove names of lists: NOW, It works as expected ...
input <- list(rand_fn, rand_arg)
pmap(.l = input,
     .f = function(fn, args) exec(.fn = fn, !!!args))
#> $rnorm_fn
#> [1] 0.2278993 0.6803844
#> 
#> $runif_fn
#> [1] 0.4297577 0.5496519
hadley commented 2 years ago

Can you please make your reprex a bit more minimal? It's not clear to me precisely what problem you're seeing.

siavash-babaei commented 2 years ago

Hi, Sorry (for long reprex), and Thank You (for all in everything("tidy")) @hadley.

hadley commented 2 years ago

Ok, here's an even more minimal reprex:

library(purrr)

fns    <- list(rnorm_fn = rnorm, runif_fn = runif)
args <- list(
  rnorm_args = list(n = 2, mean = 0, sd  = 1),
  runif_args = list(n = 2, min  = 0, max = 1)
)

# ok
x <- pmap(list(fns = fns, args = args), ~ exec(.x, !!!.y))

# fails
x <- pmap(list(fns = fns, args = args), \(fn, args) exec(fn, !!!args))
#> Error in `pmap()`:
#> ℹ In index: 1.
#> Caused by error in `.f()`:
#> ! unused argument (fns = .l[[1]][[i]])

Created on 2022-10-31 with reprex v2.0.2

You can see the problem a bit more easily if you add ... to the function you're applying:

x <- pmap(list(fns = fns, args = args), \(fn, args, ...) exec(fn, !!!args))
#> Error in `pmap()`:
#> ℹ In index: 1.
#> Caused by error in `exec()`:
#> ! argument "fn" is missing, with no default

Created on 2022-10-31 with reprex v2.0.2

By design, when the input pmap() is named, those names are matched to the names of the functions.

It only really works with the formula form by coincidence because of the way that as_mapper() converts the formula to a function:

as_mapper(~ exec(.x, !!!.y))
#> <lambda>
#> function (..., .x = ..1, .y = ..2, . = ..1) 
#> exec(.x, !!!.y)
#> attr(,"class")
#> [1] "rlang_lambda_function" "function"

This is one of the reasons that the dev version recommends against using ~ with pmap(): https://purrr.tidyverse.org/dev/reference/pmap.html#arguments