44

I am using the map function of the purrr package in R which gives as output a list. Now I would like the output to be a named list based on the input. An example is given below.

input <- c("a", "b", "c")
output <- purrr::map(input, function(x) {paste0("test-", x)})

From this I would like to access elements of the list using:

output$a

Or

output$b
Michael
  • 1,281
  • 1
  • 17
  • 32

3 Answers3

42

We just need to name the list

names(output) <- input

and then extract the elements based on the name

output$a
#[1] "test-a"

If this needs to be done using tidyverse

library(tidyverse)
output <- map(input, ~paste0('test-', .)) %>% 
                                setNames(input)
akrun
  • 874,273
  • 37
  • 540
  • 662
  • 2
    FYI `purrr` imports `set_names()` from `rlang` for this reason. Not that there is any significant difference between `base::setNames()`... – Anders Swanson Jun 29 '18 at 21:58
  • 10
    @AndersSwanson There are significant differences, and in particular you can do `x %>% set_names() %>% map(fn)` which is the recommended solution to this problem. – Lionel Henry Oct 11 '19 at 07:14
33

The recommended solution:

c("a", "b", "c") %>% 
    purrr::set_names() %>% 
    purrr::map(~paste0('test-', .))
mihagazvoda
  • 1,057
  • 13
  • 23
  • 3
    Brilliant. We set the name in advance. This also works with *map_dfc*, and avoids the "renaming warnings for the duplicate names" that I get when setting the name after the map. – vinnief Oct 12 '20 at 13:13
31

Update

Now in 2020 the answer form @mihagazvoda describes the correct approach: simply set_names before applying map

c("a", "b", "c") %>% 
    purrr::set_names() %>% 
    purrr::map(~paste0('test-', .))

Outdated answer

The accepted solution works, but suffers from a repeated argument (input) which may cause errors and interrupts the flow when using piping with %>%.

An alternative solution would be to use a bit more power of the %>% operator

1:5 %>% { set_names(map(., ~ .x + 3), .) } %>% print # ... or something else

This takes the argument from the pipe but still lacks some beauty. An alternative could be a small helper method such as

map_named = function(x, ...) map(x, ...) %>% set_names(x)

1:5 %>% map_named(~ .x + 1)

This already looks more pretty and elegant. And would be my preferred solution.

Finally, we could even overwrite purrr::map in case the argument is a character or integer vector and produce a named list in such a case.

map = function(x, ...){
    if (is.integer(x) | is.character(x)) {
        purrr::map(x, ...) %>% set_names(x)
    }else {
        purrr::map(x, ...) 
    }
}

1 : 5 %>% map(~ .x + 1)

However, the optimal solution would be if purrr would implement such behaviour out of the box.

Holger Brandl
  • 10,634
  • 3
  • 64
  • 63
  • That would be awesome! Shouldn't you propose such a pull request? Or have they a good reason not to do this that way? – Dan Chaltiel Jul 11 '19 at 13:43
  • 2
    My gut feeling would be, that the `purrr` maintainers do not like it because it creates different semantics depending on the input. I'd imagine that they prefer some more explicit approach, like with a parameter, but this would support the usecase as well. Also, it would break backward compatibility in quite a few cases I guess. But sure, why not asking, see https://github.com/tidyverse/purrr/issues/691 – Holger Brandl Jul 11 '19 at 20:23
  • The recommended solution (added a year later by @mihagazvoda) shows there is indeed a purrrring way of doing this. – vinnief Oct 12 '20 at 13:04
  • Indeed. I don't want to delete the answer, but rather cast the upvotes to this answer to @mihagazvoda. Is there a stackoverflow way to do so? – Holger Brandl Oct 14 '20 at 18:58