purrr 0.2.0を使ってみる

purrr 0.2.0がCRANにきてました。けっこう色々変更があります。

dplyrとの兼ね合いでどこを変えるかまだ試行錯誤している雰囲気があって、まだまだbreaking changeが入りそうな雰囲気です。がっつり使うのはまだ控えたほうがいいかも...。

新しい関数

%||%

x %||% y is shorthand for if (is.null(x)) y else x (#109).

xNULLだったらyの値を返す中置演算子です。

1 %||% 2
#> [1] 1
NULL %||% 2
#> [1] 2

%@%

x %@% "a" is shorthand for attr(x, "a", exact = TRUE) (#69).

attributeを取り出す中置演算子です。

matrix(1:6, nrow = 2) %@% "dim"
#> [1] 2 3

accumulate()

accumulate() has been added to handle recursive folding.

baseの関数Reduce()の方はaccumerate = TRUEという引数があって、

Reduce(c, 1:5, 0, accumulate = TRUE)

みたいに結果を貯めていくことができたんですが、これをpurrrでは↓のように書けるようになります。

1:5 %>% accumulate(c)

Reduce()init引数にあたる.init引数を指定することもできます。

1:10 %>% accumulate(max, .init = 5)
#> [1]  5  5  5  5  5  5  6  7  8  9 10

右から折りたたみたいときにはaccumulate_right()reduce_right()があるらしいです。

1:10 %>% accumulate_right(min, .init = 5)
#> [1] 1 2 3 4 5 5 5 5 5 5 5

map_df()

map_df() row-binds output together. It's the equivalent of plyr::ldply()(#127)

これ便利です。

これまで、map()した結果を一つのdata.frameにするのはdplyr::bind_rows()を呼び出してましたが、

1:10 %>%
  map(~ list(waru10 = ./10, kakeru5 = .*5)) %>%
  dplyr::bind_rows()

map_df()できるようになります。

1:10 %>%
  map_df(~ list(waru10 = ./10, kakeru5 = .*5))
#> Source: local data frame [10 x 2]
#> 
#>    waru10 kakeru5
#>     (dbl)   (dbl)
#> 1     0.1       5
#> 2     0.2      10
#> 3     0.3      15
#> 4     0.4      20
#> 5     0.5      25

あれ? listなのに_dfだと型が違うってエラーにならないの?と思ったそこのアナタ(たぶんいない…)

そうそう、purrrはちょっと型に対して甘くなりました。型が変換できるときは変換する、というポリシーになったのです。これNEWSに載せないのかな...

ということで、Japan.Rでの私のつぶやきは今や嘘です。忘れてください。

flatten()の挙動変更(flatmap()のは廃止)、flatten()の型指定版

flatten() is now type-stable and always returns a list. To return a simpler vector, use flatten_lgl(), flatten_int(), flatten_dbl(), flatten_chr(), or flatten_df().

flatmap() -> use map() followed by the appropriate flatten().

flatmap()がdeprecatedになりました。また、flatten()にも型を指定するバージョンの関数ができました。

flatten()unlist()的なやつです。あんまり前の挙動を覚えてないので変化がピンとこないんですが、、こういう動きです。

x <- map(2:3, sample)
x
#> [[1]]
#> [1] 1 2
#> 
#> [[2]]
#> [1] 3 1 2

x %>% flatten()
#> [[1]]
#> [1] 4
#> 
#> [[2]]
#> [1] 1
#> 
#> [[3]]
#> [1] 3
#> 
#> [[4]]
#> [1] 2
#> 
#> [[5]]
#> [1] 1
#> 
#> [[6]]
#> [1] 3
#> 
#> [[7]]
#> [1] 2
#> 
#> [[8]]
#> [1] 4

平らなlist()になってますね。これをvectorにまとめるには、flatten_XXXを使います。intはInteger、dblはdouble、chrはcharacter、lglはlogical、dfはdata.frameです。

x %>% flatten_int()
#> [1] 4 1 3 2 1 3 2 4

invoke()の挙動変更(map_call()は廃止)、invoke_map()

invoke() has been overhauled to be more useful: it now works similarly to map_call() when .x is NULL, and hence map_call() has been deprecated.

invoke()do.call()の単純なラッパーになりました。関数ひとつを適用するだけです。これに伴って、map_call()が非推奨になりました。

invoke(runif, list(n = 10))
#> [1] 0.56532877 0.02724676 0.60148070 0.07418137 0.16318623 0.04143166
#> [7] 0.72120653 0.39732728 0.95660030 0.11342016
do.call(runif, list(n = 10))
#> [1] 0.2695608 0.3741370 0.3705948 0.7065786 0.3497762 0.7854298 0.6258898
#> [8] 0.3753446 0.3891058 0.7384476

関数の引数をlistじゃなくて名前付き引数でも渡せるのがdo.call()と微妙に違います。

do.call(runif, n = 10)
#> Error in do.call(runif, n = 10) : unused argument (n = 10)
invoke(runif, list(n = 10))
#> [1] 0.07460899 0.41545157 0.35268372 0.02313332 0.60232309 0.52617241
#> [7] 0.84474122 0.73989323 0.67794054 0.96395660

複数の関数を適用にするには、新しいinvoke_map()を使います。

list(m1 = mean, m2 = median) %>% invoke_map(x = rcauchy(100))
#> $m1
#> [1] 1.669889
#> 
#> $m2
#> [1] 0.09461299

flatten_XXX()map_XXX()のように、型指定の関数もあります。

list(m1 = mean, m2 = median) %>% invoke_map_dbl(x = rcauchy(100))
#>          m1          m2 
#>  1.57018475 -0.04951891 

transpose()zipX()は廃止)、simplify_all()

transpose() replaces zip2(), zip3(), and zip_n() (#128).

It no longer has fields argument or the .simplify argument; instead use the new simplify_all() function.

他の言語とちょっと違って分かりづらかったzipX()は廃止されました。transpose()は、名前の通り、listを転置する関数です。こういう動きをします。2回転置すると元に戻ります。

set.seed(1)
x <- rerun(3, x = runif(1), y = runif(3))

x %>% str()
#> List of 3
#>  $ :List of 2
#>   ..$ x: num 0.266
#>   ..$ y: num [1:3] 0.372 0.573 0.908
#>  $ :List of 2
#>   ..$ x: num 0.202
#>   ..$ y: num [1:3] 0.898 0.945 0.661
#>  $ :List of 2
#>   ..$ x: num 0.629
#>   ..$ y: num [1:3] 0.0618 0.206 0.1766

x %>% transpose() %>% str()
#> List of 2
#>  $ x:List of 3
#>   ..$ : num 0.266
#>   ..$ : num 0.202
#>   ..$ : num 0.629
#>  $ y:List of 3
#>   ..$ : num [1:3] 0.372 0.573 0.908
#>   ..$ : num [1:3] 0.898 0.945 0.661
#>   ..$ : num [1:3] 0.0618 0.206 0.1766

x %>% transpose() %>% transpose() %>% str()
#> List of 3
#>  $ :List of 2
#>   ..$ x: num 0.266
#>   ..$ y: num [1:3] 0.372 0.573 0.908
#>  $ :List of 2
#>   ..$ x: num 0.202
#>   ..$ y: num [1:3] 0.898 0.945 0.661
#>  $ :List of 2
#>   ..$ x: num 0.629
#>   ..$ y: num [1:3] 0.0618 0.206 0.1766

simplify_all()を使うと、listの各要素を可能な限りvectorにまとめてくれます。

x %>% transpose() %>% simplify_all()
#> $x
#> [1] 0.2655087 0.2016819 0.6291140
#> 
#> $y
#> [1] 0.37212390 0.57285336 0.90820779 0.89838968 0.94467527 0.66079779
#> [7] 0.06178627 0.20597457 0.17655675

safely()quietly()possibly()

safely(), quietly(), and possibly() are experimental functions for working with functions with side-effects (e.g. printed output, messages, warnings, and errors) (#120).

これは、はじめはHaskellとかでいうMaybeをつくろうとしてたみたいなんですが、結果とエラーとメッセージをリストとして返す、というものになりました。

こんな感じです。

safe_log <- safely(log)
safe_log(10)
#> $result
#> [1] 2.302585
#> 
#> $error
#> NULL

safe_log("a")
#> $result
#> NULL
#> 
#> $error
#> <simpleError in .f(...): non-numeric argument to mathematical function>

otherwise引数を指定すれば、エラーになった場合でもresultに値が入ります。これでMaybeっぽい使い方ができるかもしれません。

list_along(), rep_along()

list_along() and rep_along() generalise the idea of seq_along().

list_along()は、引数に指定したオブジェクトと同じ長さのlistを返します。rep_along()は、第一引数に指定したオブジェクトの長さだけ、第二引数のオブジェクトを繰り返します。

x <- 1:5

rep_along(x, 1:2)
#> [1] 1 2 1 2 1

list_along(x)
#> [[1]]
#> NULL
#> 
#> [[2]]
#> NULL
#> 
#> [[3]]
#> NULL
#> 
#> [[4]]
#> NULL
#> 
#> [[5]]
#> NULL

is_null()

is_null() is the snake-case version of is.null().

スネークケースになっただけみたいです。もはや意地ですね…。

この辺のpredicate functionは別のパッケージに移そうという計画もあるみたいです。

pmap()map_n()は廃止)

pmap() (parallel map) replaces map_n() (#132)

map_n()は、pmap()にリネームされました。またpmap()map()と同じくlglとかintとかいう型指定版があります。

つまり、まとめるとこんな感じです。

返り値の型 引数が1つ 引数が2つ 引数が3つ以上
logical map_lgl() map2_lgl() pmap_lgl()
integer map_int() map2_int() pmap_int()
double map_dbl() map2_dbl() pmap_dbl()
character map_chr() map2_chr() pmap_chr()
data.frame map_df() map2_df() pmap_df()

set_names()

set_names() is a snake-case alternative to setNames() with stricter equality checking

setNames()よりちょっと引数チェックが厳しい関数らしいです。よく分からない。(知らない方も多いかもしれませんが、setNames()は、名前つきベクトルを一発でつくれる便利な関数です)

set_names(1:4, c("a", "b", "c", "d"))
#> a b c d 
#> 1 2 3 4 

名前を省略すると、それぞれの要素がそのまま名前になります。

set_names(letters[1:5])
#>   a   b   c   d   e 
#> "a" "b" "c" "d" "e" 

行指向の関数

行指向とはどういうことかというと、dplyrみたいな感じのことができるようになるということです。

dmap(), dmap_at(), dmap_if()

map() now always returns a list. Data frame support has been moved to map_df() and dmap(). The latter supports sliced data frames as a shortcut for the combination of by_slice() and dmap(): x %>% by_slice(dmap, fun, .collate = "rows"). The conditional variants dmap_at() and dmap_if() also support sliced data frames and will recycle scalar results to the slice size.

dmap()は、dplyr::mutate_each()みたいなものです。slice_rows()group_by()のラッパーみたいなものです(grouped_dfが返ってくる)。

# slice_rows("cyl")の代わりにgroup_by(cyl)にしても同じ
sliced_df <- mtcars[1:5] %>% slice_rows("cyl")

sliced_df %>% dmap(mean)
#> Source: local data frame [3 x 5]
#> 
#>     cyl      mpg     disp        hp     drat
#>   (dbl)    (dbl)    (dbl)     (dbl)    (dbl)
#> 1     4 26.66364 105.1364  82.63636 4.070909
#> 2     6 19.74286 183.3143 122.28571 3.585714
#> 3     8 15.10000 353.1000 209.21429 3.229286

これと同じことをdmap()を使わずにやろうと思うと、たぶん↓こんな感じですがよく分かりませんでした。

mtcars[1:5] %>%
  split(.$cyl) %>% map(map_dbl, mean) 

めちゃくちゃ便利そうですが、dplyrとの使い分けが気になるところです。

invoke_rows()map_rows()の代わり)

map_rows() has been renamed to invoke_rows().

これむずくてよく分からなかったのでパス。。

.to引数、.collate引数

The rows-based functionals gain a .to option to name the output column as well as a .collate argument. The latter allows to collate the output in lists (by default), on columns or on rows. This makes these functions more flexible and more predictable.

これはどういうことかというと、行指向の関数は、新しい列をつくって結果をそこに格納します。.to引数はそのときの名前を選びます。デフォルトだと.outという名前になります。

mtcars[1:2] %>% by_row(function(x) 1:5)
#> Source: local data frame [32 x 3]
#> 
#>      mpg   cyl     .out
#>    (dbl) (dbl)   (list)
#> 1   21.0     6 <int[5]>
#> 2   21.0     6 <int[5]>
#> 3   22.8     4 <int[5]>
#> 4   21.4     6 <int[5]>
#> 5   18.7     8 <int[5]>
#> ..   ...   ...      ...

.collate引数は、その結果をどう展開するかを指定します。"list"だと上のようにネストした状態で格納されます。"rows"だと行方向に展開されます。元のdata.frameと.outをjoinするようなイメージです。

mtcars[1:2] %>% by_row(function(x) 1:5, .collate = "rows")
#> Source: local data frame [160 x 4]
#> 
#>      mpg   cyl  .row  .out
#>    (dbl) (dbl) (int) (int)
#> 1     21     6     1     1
#> 2     21     6     1     2
#> 3     21     6     1     3
#> 4     21     6     1     4
#> 5     21     6     1     5
#> ..   ...   ...   ...   ...

"cols"だと列方向に展開されます。その分、列が増えます。

mtcars[1:2] %>% by_row(function(x) 1:5, .collate = "cols")
#> Source: local data frame [32 x 7]
#> 
#>      mpg   cyl .out1 .out2 .out3 .out4 .out5
#>    (dbl) (dbl) (int) (int) (int) (int) (int)
#> 1   21.0     6     1     2     3     4     5
#> 2   21.0     6     1     2     3     4     5
#> 3   22.8     4     1     2     3     4     5
#> 4   21.4     6     1     2     3     4     5
#> 5   18.7     8     1     2     3     4     5
#> ..   ...   ...   ...   ...   ...   ...   ...

その他

as_function()

as_function(), which converts formulas etc to functions, is now exported (#123).

これは、map()などなどpurrrの関数の中でラムダ関数をつくるのに使われている関数です。具体的には、以下のような挙動をします。

If a function, it is used as is.
If a formula, e.g. ~ .x + 2, it is converted to a function with two arguments, .x or . and .y. This allows you to create very compact anonymous functions with up to two inputs.
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"]].

引数が関数なら、そのまま関数が返ってきます。
引数がformulaなら、..xを第一引数、.yを第二引数とする関数に変換します。 引数がcharacterかnumericなら、ベクトルからそのインデックスの要素を取り出す関数に変換します。例えば、as_function(1)は、1つ目の要素を取り出す関数です。

Cで実装

map*() now use custom C code, rather than relying on lapply(), mapply() etc. The performance characteristcs are very similar, but it allows us greater control over the output (#118).

これまでは単純にlapply()mapply()の単なるラッパーでしたが、独自にCで実装したものを使うようになりました。これによってパフォーマンス面での向上が見込まれ、るかと思いきやそこはあんまり変わらなくて、出力をもっとうまくコントロールできるようになるとかなんとか。よくわかりません。

感想

予想通り?、dplyrの領分にも浸食し出したpurrr。これからも目が離せません。心配という意味で。下手をするとdplyrをぶっ壊しかねないのでは…と危惧しています。