R 4.1で入ると噂の|>が開発版のRに入ったので触ってみた。

magrittr 2.0のブログ記事で仄めかされていたように、base Rに(というかRの文法レベルで)ネイティブのパイプ演算子|>が入るようです。 それがとうとう開発版のRに実装された、というツイートでTLがざわついていたので触ってみました。

Dockerでさくっとrocker/tidyverse:develを試すことにします。 なお、当然ですが開発版なので今後大きく変更されることもあります。 あくまで参考までに。

docker run --rm -it rocker/tidyverse:devel R

基本的な使い方

基本的な使い方は%>%と同じです。 |>の左にある表現を右の関数呼び出しの第一引数に放り込んでくれます。 以下の例でいうと、mtcarshead(2)2を押しのけて第一引数に割り込み、head(mtcars, 2)として評価される、という寸法です。

mtcars |> head(2)
#>               mpg cyl disp  hp drat    wt  qsec vs am gear carb
#> Mazda RX4      21   6  160 110  3.9 2.620 16.46  0  1    4    4
#> Mazda RX4 Wag  21   6  160 110  3.9 2.875 17.02  0  1    4    4

|>%>%の違い: 右辺は関数呼び出ししか受け付けない

さて、基本は同じだと確認したところで、まずはニッチな違いから見ていきましょう。 %>%にはいろいろ察していい感じに処理してくれる、みたいな機能がありました。 たとえば、%>%には右辺が関数呼び出し(()あり)ではなく関数オブジェクト(()なし)でも動きました。

mtcars %>% head
#>                    mpg cyl disp  hp drat    wt  qsec vs am gear carb
#> Mazda RX4         21.0   6  160 110 3.90 2.620 16.46  0  1    4    4
#> Mazda RX4 Wag     21.0   6  160 110 3.90 2.875 17.02  0  1    4    4
#> Datsun 710        22.8   4  108  93 3.85 2.320 18.61  1  1    4    1
#> Hornet 4 Drive    21.4   6  258 110 3.08 3.215 19.44  1  0    3    1
#> Hornet Sportabout 18.7   8  360 175 3.15 3.440 17.02  0  0    3    2
#> Valiant           18.1   6  225 105 2.76 3.460 20.22  1  0    3    1

ところが、|>でこれは動きません。 動かない、というのも、実行されてエラーになるのではなく、文法レベルでエラーになるので reprex::reprex() すら実行できません。

> reprex::reprex({ mtcars |> head })
Error: The pipe operator requires a function call or an anonymous function expression as RHS

不便かもしれませんが、個人的にはこれは好ましい挙動だと考えています。 %>%の挙動は便利ですが、たとえば::でパッケージ名を指定するとエラーになる、という困った問題があります。

mtcars %>% utils::head
#> Error in .::utils: unused argument (head)

これは :: も関数で utils::head は関数呼び出しだ、ということに由来するのですが、ふつうのRユーザーにとっては謎のエラーすぎます。 なのでそもそもmagrittrは()なしを許すべきではなかった、とmagrittr作者の人もたしかツイートしてたんですが(うろ覚え)、 あまりにも一般的になりすぎたので今さらbreaking changeを入れられない、ということで今に至っています。

|>%>%の違い: |>は文法要素

上で「文法レベルでエラーになる」と書きましたが、実は、|>関数ではありません。 私はこのツイートで知っただけなんですが、なるほどなーと思ったので紹介します。 実際、以下のようにコンソールに打ってみるとわかりますが|>という関数は存在しません。

`|>`
#> Error: object '|>' not found

これは、->と同じです。 ->は関数としては存在しません。

`->`
#> Error: object '->' not found

ではどうやって動いているのかというと、パーサによって、左辺と右辺を逆にして通常の<-を使った式に変形する、ということが起こっています。

quote(1 -> a)
#> a <- 1

|>も同じで、パーサのレベルで式が変形されています。

quote(mtcars |> head())
#> head(mtcars)

つまり、magrittr 2.0でも%>%はかなり高速になりましたが、|>は関数呼び出しのオーバーヘッドすらない、ということです。

|>%>%の違い: 第一引数以外に渡したいときは関数でラップする

|>は、左辺を第一引数以外に渡したいときは少し面倒です。 例えば、rep()に繰り返し回数を左辺から渡したい場合を考えてみましょう。rep()は第一引数に繰り返される要素を、第二引数に繰り返す回数を取ります。 こんなとき、%>%では.を使うことで渡したい引数の場所を指定することができました。

2 %>% rep(1, .)

しかし、|>は第一引数に渡すことしかできません。 なので、第二引数以降に渡すためには、次のように関数でラップして、そのラップした関数に渡してやる必要があります。

2 |> (function(n) rep(1, n))()

さすがにこれは冗長なので、その場で関数を定義する場合は関数呼び出し()の省略を許すことにしているようです。

2 |> function(n) rep(1, n)

これでもちょっとfunction()をタイプするのはめんどくさいですね。 そこで(このためだけなのかは謎ですが)、関数の定義に\()という短縮表現が使えるようになりました。 \(引数) 内容のように指定します。これを使うと、かなり短く書くことができます。

2 |> \(n) rep(1, n)

ちなみに\()|>の外でも普通に使えます。これもパーサのレベルで処理されるもので、完全にfunction()と同じです。

quote(\(x) x)
#> function(x) x

quote(\(x, y, ...) c(x, y, ...))
#> function(x, y, ...) c(x, y, ...)

なので、lapply()の中で無名関数を作るのに使ったり、これでつくった関数を変数に代入したりできます。

(R-develメーリングリストでは、「これでもまだ長くね?」という話が出ているので、今後さらに変わるかもしれません)

追記(2020/12/5)

いろいろ触っていて気づいたんですが、[とか中置演算子とか文法要素っぽい関数は受け付けないっぽいですね。 なぜこういうデザインなんだろう...

感想

まさかここで関数作成のショートカットまで出てくるとは思ってませんでしたが、%>%と比べてだいぶシンプルに実装されてるなという印象です。 最終的にR 4.1のときにどうなっているかわかりませんが、待ち遠しいですね。