読者です 読者をやめる 読者になる 読者になる

メモ:purrr::map()は深い階層の要素でも一発で取り出せる

R4DSに書いてある話。

たとえばこういう要素があるとする。

l <- list(
  group1 = list(
    name = "group1",
    score = 10,
    user = list(
      loginname = "user1",
      emoji = ":sushi:"
    )
  ),
  group2 = list(
    name = "group2",
    score = 11,
    user = list(
      loginname = "user2",
      emoji = ":llap:"
    )
  )
)

ここで、nameを取り出すには"name"を指定する。

l %>%
  map("name")
#> $group1
#> [1] "group1"
#> 
#> $group2
#> [1] "group2"

では、userloginnameを取り出すにはどうするのか。単純に考えれば、2回map()すればいい。

l %>%
  map("user") %>%
  map("loginname")
#> $group1
#> [1] "user1"
#> 
#> $group2
#> [1] "user2"

でも、?mapを見るとこんなことが書かれている。

If character or integer vector, e.g. "y", it is converted to an extractor function, function(x) x[["y"]]. To index deeply into a nested list, use multiple values; c("x", "y") is equivalent to z[["x"]][["y"]].

ということで、こんな風に書ける。

l %>%
  map(c("user", "loginname"))
#> $group1
#> [1] "user1"
#> 
#> $group2
#> [1] "user2"

ちなみに、こういうセマンティクスを採用したので、たとえばnamescoreを取ってきたい、というときに

l %>%
  map(c("name", "score"))

とか書くことはできない。名前を並べていくとどんどん深い階層に潜っていくというもので、同じ階層にあるものを並列で取り出すというセマンティクスではない。でもそれは自明ではないので混乱するよね、というのがこのissueでの議論。

github.com

ちなみに、じゃあ今のところの正解は何かというと、

l %>%
  map(`[`, c("name", "score"))

らしい。

でもこれははっきり言って事故の元だと思う。要素名を直書きで指定している場合はいいけど、

x <- "name"

l %>%
  map(x)

みたいなコードがあったときに、xc("user", "loginname")とか、挙句の果てに1:10とかが入っていてもうっかり動いてしまってエラーにならない。黙ってNULLが返される。

x <- 1:10

l %>%
  map(x)
#> [[1]]
#> NULL
#> 
#> [[2]]
#> NULL

うーん。とりあえずissueを立ててみようかな…


追記(2016/09/03):

よくよく考えると、少なくとも文字列でインデックスを指定した時は、存在しない要素名でもエラーにならないのでした。

x <- list()

x[["ninja"]]
#> NULL

ネストしても大丈夫。

x[["ninja"]][["why"]][["ninja"]]
#> NULL

というのは、[[NULLに対しても使えるからです。

NULL[["wasshoi!"]]
#> NULL

数値のインデックスを指定した時だけエラーになります。

x[[1]]
#> Error in x[[1]] : subscript out of bounds

数値のインデックスでも、NULLに対してやったときはエラーになりません。

NULL[[1]]
#> NULL
NULL[[1]][[2]]
#> NULL

わけが分からないよ…


追記2(2016/09/03):

よく見るとうさぎさんのブログにちゃんと書いてました。そんなことできたのか。

補足 第二引数にベクトルを渡した場合の処理は、以下のように階層的な選択になる ( l[[c('a', 'b')]] と一緒)。