Confused about this purrr::map situation

I'm confused about purrr::map2. I thought I understood it but I'm stumped here. Can someone help me understand why the first example below is not giving me the output I expected?

library(purrr)

test1 <- list(a = 3, b = 4)
test2 <- list(a = 4:6, b = 5:7)

I want to map along each of these lists and pass the elements stepwise to
a map function

# this does not give what I expect
map2(test1, test2, ~ map(.y, ~ c(.x, .)))
#> $a
#> $a[[1]]
#> [1] 4 4
#> 
#> $a[[2]]
#> [1] 5 5
#> 
#> $a[[3]]
#> [1] 6 6
#> 
#> 
#> $b
#> $b[[1]]
#> [1] 5 5
#> 
#> $b[[2]]
#> [1] 6 6
#> 
#> $b[[3]]
#> [1] 7 7

but if I take the lists element-wise and pass them in, it does what I want

map(test2[[1]], ~ c(test1[[1]], .))
#> [[1]]
#> [1] 3 4
#> 
#> [[2]]
#> [1] 3 5
#> 
#> [[3]]
#> [1] 3 6
map(test2[[2]], ~ c(test1[[2]], .))
#> [[1]]
#> [1] 4 5
#> 
#> [[2]]
#> [1] 4 6
#> 
#> [[3]]
#> [1] 4 7

and if I write a function, it also does what I expect

append_to_existing <- function(vec_x, vec_y) {
  map(vec_y, ~ c(vec_x, .))
}

# gives desired output
map2(test1, test2, append_to_existing)
#> $a
#> $a[[1]]
#> [1] 3 4
#> 
#> $a[[2]]
#> [1] 3 5
#> 
#> $a[[3]]
#> [1] 3 6
#> 
#> 
#> $b
#> $b[[1]]
#> [1] 4 5
#> 
#> $b[[2]]
#> [1] 4 6
#> 
#> $b[[3]]
#> [1] 4 7

Created on 2020-12-11 by the reprex package (v0.3.0)

I have a guess.

map2(test1, test2, ~ map(.y, ~ c(.x, .)))

Inside the map function, you wanted to pass each element of .y, which is test2 to the innermost function, where it will be concatenated to .x, the passed element of test1.

My guess that this last part is not working.

.x, the passed element of test1

Inside that innermost function, .x is taking the .x argument of the map function. So, both .x and . refer to basically the same element of .y, resulting in the unexpected answer.

I would like to be wrong here, but I faced these types of errors lot. .x, .y for first, second argument of the function seems to be very helpful, but I get confused when I have to have functions under functions under functions, and don't have control over argument names. That's why I've switched from ~, .x, .y etc. based functions to explicit standard function, like you did.

2 Likes

That's because map() can use both . or .x as an argument:

all.equal(map(1:3, ~ .),
          map(1:3, ~ .x))
#> [1] TRUE

If you want to nest maps, it''s safer to explicitly specify the arguments:

map2(test1, test2, function(x1,y1) map(y1, function(x2) c(x1, x2)))
3 Likes

Thanks both, I hadn't realised about the ambiguity with map taking .x, I thought .x and .y were just used for map2.

It's about environments. Each map has it's own environment, so each map has it's own . which is equivalent of .x. The .x is not the object (value) you expect, but you miss the environment aspect.

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.

If you have a query related to it or one of the replies, start a new topic and refer back with a link.