(5/22追記あり)[雑記] R 4.2.0のリリースでにわかに盛り上がる Base Pipe “|>” とは何なのか? %>%との違いを調べました

雑記
2022/5/22追記
Twitterにて@eitsupiさんに 「Base pipeのプレースホルダーを二回以上使用すると、その数だけパイプ以前の処理を繰り返し評価してしまう」という情報をいただきました!

この仕様を踏まえると、今後プレースホルダーが二回以上使えるようになる日は来ない可能性があるな🤔と思いました。詳しい内容は「追記」にて追記させていただきました。

はじめに

こんにちは! このブログは当初初心者向けのコンテンツを提供する場として始めたのですが、いよいよネタ切れが近づいてきたのとモチベーションが保ちにくくなってきたので、これからは雑記や最新情報なども書いていこうかなと思っています🌜

そんな決意をした一発目の話題は「最近導入されたNative pipeは何か」という、めっちゃタイムリーなやつを取り上げてみようと思います! 正直この記事のボリュームがかなり大きくなりそうな気しかしませんが、ずっと気になってたことでもあるので頑張ってまとめたいと思います。

今回は解説口調ではなく、ゆるゆるとした感じで書いていきたいとおもいます!

そもそもパイプって何?

%>% についておさらい

%>%

これをパイプ演算子と言います。英語ではpipeといいます。全く見たこともないという方はもはや居ないかもしれませんね。 Rの標準機能(base R)には存在しない関数の一つであり、データ解析言語としての特長と非常に相性が良かったため、爆発的に人気となりました。そして今ではRのデファクトスタンダードにまでなりました。

%>% 誕生の歴史

私がRの勉強を始めた2015年頃、既に{magrittr}パイプがR界隈を席巻していましたので、発生の経緯についてはよく知りませんでした。パイプの歴史について調べてみたところ、Adolfo Álvarez氏のブログポストが非常によくまとめられていました。当記事はAdolfo氏の以下のまとめを参考にさせていただきました。

Plumbers, chains, and famous painters: The (updated) history of the pipe operator in R | Adolfo Álvarez
A modern, beautiful, and easily configurable blog theme for Hugo.

その起源は2012年に"user4"という名もなきユーザーがStackoverflowのに投稿したとある質問でした。

(和訳) F#言語のパイプ演算子をRで実装するにはどうしたらよいだろうか? たとえば、foobarという関数を続けて使いたい場合、こんな感じで書きたいんだけど。

data |> foo |> bar

この質問はたった一時間後にBenjamin Bolker氏によって解決されます。

(和訳) 実際のところどれくらい使えるかは分からないけど、少なくとも引数が一つの関数ならできるっぽい(?)

"%>%" <- function(x,f) do.call(f,list(x))
pi %>% sin
[1] 1.224606e-16
pi %>% sin %>% cos
[1] 1
cos(sin(pi))
[1] 1

この方、すごいですよね%>%という関数を定義することでおおむねパイプ操作が実現されました。しかしまだまだ原始的な関数という感じです。

その後、2012年に始まったHadley Wickham氏の{plyr}パッケージプロジェクト({dplyr}の原型)にて、パイプ操作を可能にするchain()関数が導入され、2013年にはパイプ演算子%.%が導入されました。

しかし、%.%演算子は次に述べるパイプの存在ゆえ、非常に短命だったようです。2013年のStackoverflowにてStefan Bache氏が以下のようなパイプを提案します。

(和訳) これならどうだろう?

`%>%` <-
 function(e1, e2)
 {
   cl <- match.call()
   e  <- do.call(substitute, list(cl[[3]], list(. = cl[[2]])))
   eval(e)
 }

これならこういう操作が可能になるはず。

matrix(rnorm(16), 4, 4) %>% apply(., 1, sum) %>% sin(.) %>% sum(.)
iris %>% 
 subset(., Species == "setosa", select = -Species) %>% 
 colMeans(.)

Stefan氏はその後も精力的にパイプの改良を行い、この時の成果が後の{magrittr}パッケージとなったとのことでした。

Elephant at sunset
確かにmagrittrのコミットヒストリーを見ると、Stefan氏が2014年1月1日から開発を始め、そのわずか19日後にはHadley Wickham氏もコミットしています。Hadley氏も最初の最初から協力して開発していたことがうかがえます。
Info

ちなみに、{magrittr}は「マグリッター」と発音されますが、由来はルネ・マグリットの1929年の絵画作品「イメージの裏切り」だそうです。この絵画は「どれほど本物のような絵を描いても所詮は絵である。だからこの絵に描かれたパイプはパイプではない。」という芸術作品だそうです。 言わずもがなですが、"pipe"という名称からこの絵画作品を連想し、作者の"Magritt"とかけたのでしょう。粋ですね。

一方先ほど紹介したHadleyの%.%については、{Shift}キーを押しながら入力できる方が良いとの理由からHadley自身が開発を中断しました。その年のうちにRstudioで{Ctrl}+{Shift}+{M}のキーボードショートカットが実装されるなど一気に盛り上がり、かくしてmagrittrで開発された%>%が今に至るまでメインストリームとなったようです。

|> base pipe登場の経緯

登場の経緯と言っても、実際はR開発チームの中でどのような議論が行われたのか、全てが明らかになっているわけではありません。のコアデベロッパーたちがどんな議論を重ねてきたのかは計りかねますが、2019年にR開発チームから"base Rにもパイプ演算子を導入する必要はあるか?"という問いかけがコミュニティになされ、結果として2020年にβ公開されたR4.1.0にて遂に|>が 登場したようです。

正式リリースの昨年時点では|>はまだまだ貧弱で、既に様々な応用方法が研究され尽くされた%>%を全て置き換えるまでには至りませんでした。しかし、今回リリースされたR4.2.0にて機能がさらに拡張され、既存の%>%のほとんどをを|>で置き換えることが可能になりました。 そして、実用レベルになった|>を今後メインに使うことが色々なところで推奨されていたり、いろんな方のプレゼンスライドで%>%が使われなくなってきていることから、明らかに流れが変わってきているように感じます。

Info

私がこれまでの数年間R界隈を見てきた感じ、「うぉぉお!!」と盛り上がった技術が一瞬で廃れることも多々ありました(cf. pipeR)ので、確実に|>が主流になるかどうかは全く予想もつきません😓

%>%と|>の違い

そうは言っても本当に普段使いできるんかい。Rstudioでもキーボードショートカットを|>に変更するオプションがあるけど[1]RstudioとRのバージョンが新しくなければいけませんが、既に選択可能なオプションになっていますよ😃。本当に変えてもいいのかい。と疑いますよね。 というわけでtwitterでの声や、Hadley氏のR4DS等を参考に、違いを徹底的に調べてきました!

調べた結論として感じたのは、%>%|>はまあまあ違います。基本的に|>はシンプルに、軽量に動作するように作られているようです🤔

性質が同じか否かではなく、登場頻度が多く、覚えておいた方がよい順にご紹介します。

同じ点: 普通に第一引数に与える場合

パイプの性質「指定しなければ暗黙のうちに左の結果を右の関数の第一引数に与える」をそのまま利用する場合、今までのパイプと同じように使用可能です。

library(tidyverse)

starwars %>%
    filter(height > 100) %>%
    head(2)

# # A tibble: 2 × 14
#   name           height  mass hair_color skin_color eye_color
#   <chr>           <int> <dbl> <chr>      <chr>      <chr>    
# 1 Luke Skywalker    172    77 blond      fair       blue     
# 2 C-3PO             167    75 NA         gold       yellow   
# # … with 8 more variables: birth_year <dbl>, sex <chr>,
# #   gender <chr>, homeworld <chr>, species <chr>,
# #   films <list>, vehicles <list>, starships <list>
starwars |>
    filter(height > 100) |>
    head(2)

# # A tibble: 2 × 14
#   name           height  mass hair_color skin_color eye_color
#   <chr>           <int> <dbl> <chr>      <chr>      <chr>    
# 1 Luke Skywalker    172    77 blond      fair       blue     
# 2 C-3PO             167    75 NA         gold       yellow   
# # … with 8 more variables: birth_year <dbl>, sex <chr>,
# #   gender <chr>, homeworld <chr>, species <chr>,
# #   films <list>, vehicles <list>, starships <list>

違う点: parenthesesを省略できない

注意

私個人としてはバグの温床にもなり得るこの記法はそもそもお勧めしていません。

%>%は小難しい引数を指定しない場合、関数名の後の括弧[2]parenthesesというそうです。を省略することがきできます。

starwars %>% 
    na.omit() %>% 
    pull(mass) %>% 
    mean %>%
    head
# [1] 77.77241

しかし、|>ではparenthesesを省略することが許されていません。

starwars |>
    na.omit() |>
    pull(mass) |>
    mean
#  エラー: The pipe operator requires a function call as RHS

違う点: placeholderの文字が.ではなく_になる

前述の通り、パイプは暗黙のうちに第一引数を使います。しかし、第一引数以外を使いたい場合にはplaceholderと呼ばれる文字を使用します。 {magrittr}%>%におけるplaceholderは.(ドット)でした。

1:100 %>%
    mean(x = .)
# [1] 50.5

一方|>のplaceholderは_(アンダースコア)になりました。こちらのplaceholderがR4.2.0で導入された機能で、一気にbase pipeが実用的になった所以でもあります。

ここに示している例は{magrittr}のパイプと全く同じ挙動を示します。

1:100 |>
    mean(x = _)
# [1] 50.5

違う点: %>%は関数、|>は関数じゃない

パイプの歴史でも述べましたが、%>%は後付けで作られた関数です。また、自明ですが{magrittr}{tidyverse}パッケージの関数ですので、これらを読み込まない限り使えません。

`%>%`

# function (lhs, rhs) 
# {
#     lhs <- substitute(lhs)
#     rhs <- substitute(rhs)
#     kind <- 1L
#     env <- parent.frame()
#     lazy <- TRUE
#     .External2(magrittr_pipe)
# }
# <bytecode: 0x7f98618>
# <environment: namespace:magrittr>
`|>`
#  エラー:  オブジェクト '|>' がありません 

では|>の正体とは? 実はこれ、シンタックスシュガー(糖衣構文)と呼ばれるものであって、関数ではありません。理論的には関数よりも若干動作が速いとされていますが、目に見えて早いということもないと思います。

違う点: placeholderは引数名が指定されていないと使うことができない

"引数名が指定されていないと使うことができない"? なんのことだ?🤔と思われるかもしれませんが、多分見た方が早いです。

今までのパイプはplaceholderがどの引数に与えられるかを明示せずとも、R側で解釈可能であれば実行可能でした。

1:100 %>%
    mean(.)
# [1] 50.5

しかしながら、base pipeは引数名を明示的に示さなければエラーになります

1:100 |>
    mean(_)
#  エラー: pipe placeholder can only be used as a named argument

エラー文もその旨を伝えています。 以下のように引数名をきちんと指定していれば問題なく動作します。

1:100 |>
    mean(x = _)
# [1] 50.5
Info

この仕様は正直不便です👎 今後のアップデートで改善されるかもしれませんので、期待して待ちましょう。

違う点: paste(, _)できない

先に種明かししておきますが、完全に先の引数名指定×placeholderの件が原因です

ご存じpaste()関数は文字列操作では非常によく使う関数です。

c("日本語", "で", "構成", "される", "文字列", "の", "ベクトル") %>%
    paste()

# [1] "日本語"   "で"       "構成"     "される"   "文字列"  
# [6] "の"       "ベクトル"

パイプと組み合わせるなら、collapseオプションが便利です。

c("日本語", "で", "構成", "される", "文字列", "の", "ベクトル") %>%
    paste(collapse = "")
    
# "日本語で構成される文字列のベクトル"

そして、以下のようにplaceholderが使えます。(いやな予感)

c("のベクトル") %>%
    paste("日本語", .)
# [1] "日本語 のベクトル"

お察しの通り、引数名が与えられないので_のplaceholderが使えません

c("のベクトル") |>
    paste("日本語", _)
#  エラー: pipe placeholder can only be used as a named argument

ただし、placeholderさえ使わなければ以下のものはすべてワークします。

c("日本語", "で", "構成", "される", "文字列", "の", "ベクトル") |>
    paste()
# [1] "日本語"   "で"       "構成"     "される"   "文字列"  
# [6] "の"       "ベクトル"

c("日本語", "で", "構成", "される", "文字列", "の", "ベクトル") |>
    paste(collapse = "")
# [1] "日本語で構成される文字列のベクトル"

c("のベクトル") |>
    paste("日本語")
# [1] "のベクトル 日本語"

pasteでもplaceholderを使う回避策

要は引数に名前があればいいわけですから、強引に引数名を含む関数を定義すれば問題なく動作します。

named_paste <- function(x, y, z) paste(x, y, z)
"のベクトル" |>
    named_paste("日本語", y = _, z = "です")

# [1] "日本語 のベクトル です"

違う点: .[[]]が使えなくなる

今までのパイプはplaceholderと[[]]を組み合わせて使用することができました。

starwars %>% 
    .[[1]] %>%
    head()
# [1] "Luke Skywalker" "C-3PO"          "R2-D2"         
# [4] "Darth Vader"    "Leia Organa"    "Owen Lars"    

この使い方、「ほんとはあんまり推奨されてる使い方じゃないんだろうな~」と思いつつ、めちゃくちゃ便利なので私は非常によく使ってました😅

しかし残念ながら、base pipeではこれを使用することができません。

starwars |>
    _[[1]] |>
    head()
# エラー: pipe placeholder can only be used as a named argument

twitterで強引にこれを攻略する方法が紹介されていました。

https://twitter.com/rdataberlin/status/1/519041823490117632?s=20&t=qF2IS-D6U2dbTqyBm-Zrfw

要は関数として[[を使いつつ、名前付き引数なら動く・・・というパワープレイです。。もちろん実用性のある使い方ではないです🤣

starwars |>
    `[[`(test = _, 1) |>
    head()
# [1] "Luke Skywalker" "C-3PO"          "R2-D2"         
# [4] "Darth Vader"    "Leia Organa"    "Owen Lars"     

違う点: placeholderは二つ以上使うことができない

placeholderは第一引数以外の引数を使うために存在するわけですが、当然ながらplaceholderを二回以上使いたいときがあります。

1:10 %>% 
    rnorm(n = ., mean = .)

#  [1]  0.9856547  0.9521140  1.9552692  3.6821539  4.9358182
#  [6]  5.3278767  6.2497195  9.2191665  9.0615099 11.5664500

そして、|>のplaceholderである_は二回以上使うことができません😨

1:10 %>% 
    rnorm(n = _, mean = _)
# 1:10 %>% rnorm(n = "_", mean = "_") でエラー: 
#  invalid use of pipe placeholder
Info

私の勝手な印象ですが、「なんでもかんでもパイプが途切れることなくデータ変形するのがtidyverseの美学」 みたいな風潮がある気がします。その延長として、パイプワンライナーチャレンジ的なテクニックを見せつける猛者がいるのですが、こういった場合にplaceholderが二つ以上使われている印象があります。

基本的に、placeholderを二回以上使うようなケースは、コードの書き方次第で回避可能なはずです。

また、暗黙の第一引数に与えられたものもplaceholder一回分としてカウントされ、もう一度placeholderを使うことができません。つまりtidyverse系の関数内でplaceholderの使用は実質不可能です

starwars |>
    mutate(newcolumn = nrow(_))

# mutate(starwars, newcolumn = nrow("_")) でエラー: 
#   invalid use of pipe placeholder
Info

これは不便度レベル高いです。。今後なんらかの仕様変更があるかもしれませんね。とはいえ、最近のdplyrやtidyrの関数は泥臭くplaceholderを使わなくてもよい設計になっていますし、tidyverse側がbase pipeに合わせた仕様変更をする可能性があるかもしれませんね。

違う点: forに投げられない

これはtwitterで見て、「ほえーこんなパイプの使い方あるんだ!😲」と驚いたのですが、これまでのパイプではforループの変数に値を渡すことができたそうです。

1:10 %>% 
    for (i in .) print(i)

# [1] 1
# [1] 2
# [1] 3
# [1] 4
# [1] 5
# [1] 6
# [1] 7
# [1] 8
# [1] 9
# [1] 10

なんとなく|>では上手くいかなそうですが、やはりダメです。 この理由について調査しましたが、何故ダメなのかはわかりませんでした・・・

1:10 %>% 
    for (i in _) print(i)

#  1:10 %>% for (i in "_") print(i) でエラー: 
#   invalid use of pipe placeholder

というかfor文はやめてpurrr::map()を使いましょう

そもそもRでパイプしていて、かつループしたい時・・・for文使いません

「えっ、じゃあどうやってループするのさ!」って思われた方は今すぐpurrr::map()を覚えましょう。派生形のfurrr::future_map()を使えば簡単にマルチプロセス化できますし、何よりパイプと一緒に使う前提の設計です。

purrr::map()はちょっと慣れが必要ですし、また今度紹介記事を書きたいと思います😤

1:10 |>
    purrr::map(function(x) {print(x)})

# [1] 1
# [1] 2
# [1] 3
# [1] 4
# [1] 5
# [1] 6
# [1] 7
# [1] 8
# [1] 9
# [1] 10
# [[1]]
# [1] 1

# [[2]]
# [1] 2

# [[3]]
# [1] 3

# [[4]]
# [1] 4

# [[5]]
# [1] 5

# [[6]]
# [1] 6

# [[7]]
# [1] 7

# [[8]]
# [1] 8

# [[9]]
# [1] 9

# [[10]]
# [1] 10

同じ点: NSE(Non-standard evaluation)は可能

これまたマニアックな知識ですが、文字列をNSEの方法で評価させることは可能です。ただし、{rlang}を使って適切な方法でNSE化する必要があります。

nse_function <- 
    function(data, var) {
        data %>% 
            pull( {{var}} ) %>% 
            return()
    }
"name" %>% 
    nse_function(starwars, var = .) %>%
    head()

# [1] "Luke Skywalker" "C-3PO"          "R2-D2"         
# [4] "Darth Vader"    "Leia Organa"    "Owen Lars"   
"name" |>
    nse_function(starwars, var = _) |>
    head()

# [1] "Luke Skywalker" "C-3PO"          "R2-D2"         
# [4] "Darth Vader"    "Leia Organa"    "Owen Lars"   

番外編: リガチャーが効く

リガチャーはご存じでしょうか。リガチャー(合字)は->を並べると結合したきれいな矢印になったりする特殊なフォントのことです。 新しいパイプもきれいな三角形のリガチャーになるので、見た目的に%>%よりもいい感じです。

Info

リガチャーを手っ取り早く使ってみたい方はFira Codeがおすすめです👍

Elephant at sunset
|>はこんな三角形になります(VScode) Rstudioもリガチャに対応してますよ♪

違いをおさらい

いろいろと%>%|>が目に見えて違う例を見てきましたが、ルールを一度おさらいしてみます。

|>のルール

  • 暗黙的に第一引数へ値を渡すのが基本の使い方
  • placeholderは_を使う
  • placeholder_は二回以上同時に使うことができない
  • placeholder_を使うときは引数名=を指定しなければならない

下線を引いた二つのルールはR4.2.0現在における最大のレギュレーションって感じですね・・・。絶対困る瞬間が今後ありそうですが、少なくともR界の神、Hadley Wickham氏がbase pipeを使うことを推奨していますから、base pipeをメインに切り替えても損はないと思います。

ということで、今回は新しいbase pipeについて調べたことをまとめてみました! 久々の連休なので、のんびり過ごしま~す。ではまた♨️

追記-base pipeのプレースホルダーはその場で処理を評価している

以下のR-develメーリングリストで同じ旨の指摘があったようです(@eitupiさん情報、ありがとうございます!!)。

[Rd] brief update on the pipe operator in R-devel

私もちょっと考えてみたのですが、|>がパイプの後ろの関数に対してパイプ前の内容をまるごと放り込んでいるので、仰るとおりだなと気づきました

expr()を使って|>の正体を確認すると、状況が分かりやすいです。

magrittrのパイプはseq(10)を評価して1:10を作ってからprint()しているので、seq(10)が評価されるのは一度きりです。

expr(seq(10) %>% print(x = .))
# seq(10) %>% print(x = .)

一方、baseパイプは seq(10)print()の中でその場で評価されています

expr(seq(10) |> print(x = _))
# print(x = seq(10))

ということは、placeholderが置かれるたびに評価がやりなおされるため、二つ以上のplaceholderは非常に無駄なリソースを使用する羽目になることを意味します。

おそらく、 これが理由でplaceholderが二つ以上置くことが許されておらず、また今後も導入は難しいのではと思います

ただし別の方法で解決可能かもしれない

今記事を書いてるので、アップしたらリンク張ります!

脚注

脚注
1 RstudioとRのバージョンが新しくなければいけませんが、既に選択可能なオプションになっていますよ😃。
2 parenthesesというそうです。

コメント

タイトルとURLをコピーしました