purrr 0.1.0を使ってみる(4) lift

liftは、関数型言語の「持ち上げ」という概念ですが...、すみません、私これまったく理解できません…。

stackoverflow.com

まあ、概念はさっぱり頭に入ってきませんが、私は、Rでは「引数の形を変える」くらいの意味だと理解しています。知ってる人は知っている、do.call()のような感じです。

古のイディオム:do.call, lapply, rbind

liftについて話す前に、ちょっとおじさんっぽい話をします。

今やdplyrの隆盛によってほとんど見なくなりましたが、古の昔、力こそがすべてであり、鋼の教えと闇を司る魔が支配する時代には、

do.call(rbind, lapply(x, function(x) {...}))

というのが、xそれぞれに関数を適用してその結果を行列にする、という処理の通な書き方でした。どうですかこの力技感。

なぜこんなことが必要なのか、見てみたいと思います。まず、lapply()はlistを返します。

set.seed(1)
lapply(1:3, function(x) round(runif(10, max = x), digits = 1))
#> [[1]]
#> [1] 0.3 0.4 0.6 0.9 0.2 0.9 0.9 0.7 0.6 0.1
#> 
#> [[2]]
#> [1] 0.4 0.4 1.4 0.8 1.5 1.0 1.4 2.0 0.8 1.6
#> 
#> [[3]]
#> [1] 2.8 0.6 2.0 0.4 0.8 1.2 0.0 1.1 2.6 1.0
#>

これを行にしたmatrixをつくりたいとします。しかし、単純にこの結果をrbind()に渡すと、以下のような結果になってしまいます。

rbind(
  lapply(1:3, function(x) round(runif(10, max = x), digits = 1))
)
#> [,1]       [,2]       [,3]      
#> [1,] Numeric,10 Numeric,10 Numeric,10

これはなぜかというと、rbind()が期待する引数の形になっていないからです。rbind()の引数は、...です。

rbind(..., deparse.level = 1)

つまり、rbind(list(x, y, z))ではなく、rbind(x, y, z)の形になっていないといけません。

ここでdo.call()の登場です。do.call()は、関数と、その引数のリストを受け取って、関数を実行してくれます。

do.call(
  rbind,
  lapply(1:3, function(x) round(runif(10, max = x), digits = 1))
)
#>      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
#> [1,]  0.9  0.3  0.5  0.3  0.7  0.3  0.5  0.8  0.1   0.9
#> [2,]  0.7  1.7  0.7  0.7  1.0  1.8  1.7  0.8  1.6   1.9
#> [3,]  1.3  2.1  1.2  1.0  2.3  0.6  2.1  0.4  0.7   0.4

これはつまりどういうことかと言うと、do.call()は、rbindと引数に渡したいものの形を合わせるために使われるものなのです。

lift()がやりたいことも、この「引数の形を合わせる」ということに尽きます。(実際、do.call()のラッパーのような実装です)

lift

前置きが長くなりましたが、liftの話です。

liftはlift_xy()という形になっています。xyには、d(dot、つまり...)、vvector)、l(list)が入ります。

例として、あるdata.frameの、行ごとの合計と平均を求めたいときを考えてみます。

map_n()をdata.frameに使うと、行ごとに関数が適用されます。(data.frameは、列方向のベクトルのリスト)

x <- data.frame(value1 = 1:5, value2= 11:15, value3 = 21:25)

map_n(x, sum)
#> [[1]]
#> [1] 33
#> 
#> [[2]]
#> [1] 36
#> 
#> [[3]]
#> [1] 39
#> 
#> [[4]]
#> [1] 42
#> 
#> [[5]]
#> [1] 45

問題なく動いています。

では、mean()はどうでしょう。

map_n(x, mean)
#> Error in mean.default(value1 = dots[[1L]][[1L]], value2 = dots[[2L]][[1L]],  : 
#>   argument "x" is missing, with no default

エラーになります。

これは、sum()複数の値を引数にとれる一方、mean()はひとつの引数しか取れないためです。(オプション用の引数を除く)

sum(..., na.rm = FALSE)

mean(x, ...)

複数の値を引数にとれるmean(...)がほしい…。そんなときに使うのが、liftです。

この場合は、vectorv)の引数をdot(d)に変えたいので、lift_vd()を使います。

map_n(x, lift_vd(mean))
#> [[1]]
#> [1] 11
#> 
#> [[2]]
#> [1] 12
#> 
#> [[3]]
#> [1] 13
#> 
#> [[4]]
#> [1] 14
#> 
#> [[5]]
#> [1] 15

計算できました。

このように、mapしたいけど引数の形がうまく合わない...みたいなときにliftを使うとうまく合わせることができます。

あとは細かい話がいくつか。

デフォルトの引数

デフォルトの引数を設定したい場合は、関数に続けて引数を指定します。

list(c(1:100, NA, 1000)) %>% lift_vl(mean, na.rm = TRUE)()
#> [1] 59.90099

.unnamed

mean()の場合と逆に、x, y, z,...と引数を羅列する(dot)代わりにlistやvectorで引数を渡したい場合を考えてみると、引数についている名前が問題になることがあります。

細かい話ですが、Rの引数の指定の仕方は3種類あります。

たとえば、some_func(aaa, ...)という関数があったとき、

  1. some_func(aaa = 1)のように完全名で指定(exact matching)
  2. some_func(a = 1)のように部分名で指定(partial matching)
  3. some_func(1)のように、名前を指定せず引数の場所だけで指定(positional matching)

の順に評価されます。

When calling a function you can specify arguments by position, by complete name, or by partial name. Arguments are matched first by exact name (perfect matching), then by prefix matching, and finally by position. (Advanced R - Functions)

例えば、引数にsome_func(bbb = 1)と指定すると、名前を指定しているのでexact matchingかpartial matchingでマッチングしようとして、存在しないのでエラーになります。マッチしなかったら勝手にpositional matchingでの指定を試みてくれたりはしません。

たとえば、ヘルプにはidentical()の例が上がっています。

identical(x, y, num.eq = TRUE, single.NA = TRUE, attrib.as.set = TRUE,
          ignore.bytecode = TRUE, ignore.environment = FALSE)

identical()xyという引数を最低限必要とします。これに、positional matchingで値を指定したい場合には、.unnamed = TRUEを指定します。

mtcars[c(1, 1)] %>% lift_dl(identical, .unnamed = FALSE)()
#> Error in ..f(mpg = c(21, 21, 22.8, 21.4, 18.7, 18.1, 14.3, 24.4, 22.8,  : 
#>   unused arguments (mpg = c(21, 21, ...), mpg.1 = c(21, 21, ...))

mtcars[c(1, 1)] %>% lift_dl(identical, .unnamed = TRUE)()
#> [1] TRUE

.type

map()と同じく.typeで期待する型を指定することもできます。関数の結果がこれと違うとエラーになります。

lift_vd(tolower, .type = character(1))("this", "is", "ok")
#> [1] "this" "is"   "ok"  
lift_vd(tolower, .type = logical(1))("this", "is", "fool")
#> Error: Cannot coerce .x to a vector

感想

関数の引数の形を見るって、関数型言語っぽいですね(小並感)。

あんまり理解できてないので、ボロが出ないうちに解説は打ち切ります...。ヘルプにけっこう詳しく書いてあるので、興味ある方はそっちをぜひ読んでください!