10

This is a question for which I've written failsafes in my code before, but I'm wondering if there's something more straightforward that I've missed.

I sometimes have 2 (or more) lists that contain different types of information that need to work together with a function such as map2—think a named list of ggplot objects and a named list of file paths for saving output of each. Is there a way built-in or easily added to a piped workflow to make sure list items are matched by name rather than by position?

Consider a simple example:

library(purrr)

evens <- list(a = 2, b = 4, c = 6, d = 8)
odds <- list(a = 11, d = 9, c = 7, b = 5)

map2 returns a list with the same names as the first list, and iterates by position. So the fact that items b and d are switched in odds isn't addressed, and these two calls come out with different results:

map2(evens, odds, function(l1, l2) {
  paste(l1, l2)
})
#> $a
#> [1] "2 11"
#> 
#> $b
#> [1] "4 9"
#> 
#> $c
#> [1] "6 7"
#> 
#> $d
#> [1] "8 5"

map2(odds, evens, function(l1, l2) {
  paste(l1, l2)
})
#> $a
#> [1] "11 2"
#> 
#> $d
#> [1] "9 4"
#> 
#> $c
#> [1] "7 6"
#> 
#> $b
#> [1] "5 8"

What I've done in the past is to instead use imap and use the names of the first list to extract the appropriate item in the other list, but that means no longer having that second list in my function arguments:

imap(evens, function(l1, name) {
  paste(l1, odds[[name]])
})
#> $a
#> [1] "2 11"
#> 
#> $b
#> [1] "4 5"
#> 
#> $c
#> [1] "6 7"
#> 
#> $d
#> [1] "8 9"

If I want to feel like I'm operating more evenly over both lists, I could order them each by name, but this feels clunky:

map2(
  evens[order(names(evens))],
  odds[order(names(odds))],
  function(l1, l2) paste(l1, l2)
)
# same output as previous

Or clunkier still, make a list of the two lists, order them each in another map, then pipe that into pmap since it takes a list of lists:

list(evens, odds) %>%
  map(~.[order(names(.))]) %>%
  pmap(function(l1, l2) paste(l1, l2))
# same output as previous

Ideally, I'd like to combine the safety of the imap option with the cleanliness of map2.

camille
  • 16,432
  • 18
  • 38
  • 60
  • Maybe use something like `transpose(lst(odds, evens))` and use a standard `map` on top ? Or set your paths as attributes of your ggplot objects, if this is your use case. – moodymudskipper Apr 23 '19 at 22:34

5 Answers5

5

We can do

library(tidyverse)
map2(evens, odds[names(evens)], str_c, sep=' ')
#$a
#[1] "2 11"

#$b
#[1] "4 5"

#$c
#[1] "6 7"

#$d
#[1] "8 9"

If both the list names are unordered, loop through the sorted names of one of the list, extract both the elements and concatenate

map(sort(names(evens)), ~ str_c(evens[[.x]], odds[[.x]], sep= ' '))

Or create an identifier for the order, then order the list elements in both the list and concatenate with map2

i1 <- order(names(evens)) # not sure if this should be avoided
map2(evens[i1], odds[i1], str_c, sep=" ")
akrun
  • 874,273
  • 37
  • 540
  • 662
  • 2
    I like the solutioin of just subsetting one to the names of the other. If you use `order` you'll get deceptive results if one list has a name not in the other list – IceCreamToucan Apr 22 '19 at 18:51
  • 1
    @IceCreamToucan There are some `what if`s conditions based on the example. It could be a bit complicated if the example changes – akrun Apr 22 '19 at 18:53
5

Just write a helper function to clean it up

namemap <- function(.x, .y, .f, ...) {
  n <- order(unique(names(.x), names(.y)))
  map2(.x[n], .y[n], .f, ...)
}
namemap(odds, evens, paste)

Basically there's no primitive in purrr that will do this automatically for you. And when it's this easy to do, there doesn't seem to be much point.

MrFlick
  • 195,160
  • 17
  • 277
  • 295
  • Thanks, I don't know why I'm often wary of just writing helper functions—don't want to lose them in the ether of R scripts. But since I've already written 2 packages of utility functions for work, this is probably the safest and most sustainable way to go. – camille Apr 23 '19 at 17:51
5

bind_rows matches the names, so you could bind_rows and then map (although this imposes additional constraints on what's in the lists)

library(tidyverse)

bind_rows(evens, odds) %>% 
  map(paste, collapse = ' ')

# $`a`
# [1] "2 11"
# 
# $b
# [1] "4 5"
# 
# $c
# [1] "6 7"
# 
# $d
# [1] "8 9"
IceCreamToucan
  • 28,083
  • 2
  • 22
  • 38
2

transpose() seems to be doing this (matching by name). Though it's not documented (edit: the explanation of the .names arg gives context, and there are examples), and the documentation appears to be inaccurate in places (purrr v. 0.3.1).

It's called transpose because x[[1]][[2]] is equivalent to transpose(x)[[2]][[1]].

^ seems to be inaccurate since in this instance, list(evens, odds)[[2]][[4]] is 5 and transpose(list(evens, odds))[[4]][[2]] is 9.

Also

Note that transpose() is its own inverse, much like the transpose operation on a matrix. You can get back the original input by transposing it twice.

is not entirely accurate, but we could use that to our advantage:

list(evens, odds) %>% 
  transpose() %>% 
  transpose()
#> [[1]]
#> [[1]]$a
#> [1] 2
#> 
#> [[1]]$b
#> [1] 4
#> 
#> [[1]]$c
#> [1] 6
#> 
#> [[1]]$d
#> [1] 8
#> 
#> 
#> [[2]]
#> [[2]]$a
#> [1] 11
#> 
#> [[2]]$b
#> [1] 5
#> 
#> [[2]]$c
#> [1] 7
#> 
#> [[2]]$d
#> [1] 9

Created on 2019-04-23 by the reprex package (v0.2.1)

OP's first example ("think a named list of ggplot objects and a named list of file paths for saving output of each.") could look like:

  list(paths, plots) # or list(filename = paths, plot = plots) to match args of ggsave
  transpose() %>%
  walk(lift(ggsave))

OP's second example could be:

list(evens = evens, odds = odds) %>% # or tibble::lst(evens, odds) but lst() is in the questioning stage
  transpose() %>% 
  map(lift(paste)) # or map(paste, collapse = " ") 
#> $a
#> [1] "2 11"
#> 
#> $b
#> [1] "4 5"
#> 
#> $c
#> [1] "6 7"
#> 
#> $d
#> [1] "8 9"

Created on 2019-04-23 by the reprex package (v0.2.1)


note: I haven't checked if there exists a Github issue about this behavior, nor do I know if there is any likelihood this will change, or gain an additional argument for finer control.

Aurèle
  • 12,545
  • 1
  • 31
  • 49
1

If list names are only partially overlapping, the following modification of @MrFlick 's answer can be used. The applied function must ignore NULL parameters:

namedmap2 <- function(.x, .y, .f, ...) {
    set <- unique(c(names(.x), names(.y)))
    lst <- map2(.x[set], .y[set], .f, ...)
    names(lst) <- set
    lst
}
JohannesNE
  • 1,343
  • 9
  • 14