パイプ演算子のすすめ(種類編)

Rのパイプ演算子についての説明(種類編)
pipe
magrittr
R
Published

July 27, 2024

パイプ演算子の種類

Rでは複数の種類のパイプ演算子が知られている。 ここではいくつかのパイプ演算子について紹介したい。

現在は主に2つのパイプ演算子が使われている(|>%>%)。 それぞれのパイプ演算子の由来は以下の通り。

  • |>
    • R4.1から追加されたパイプ演算子。Rの文法要素として追加され、なにもパッケージを読み込まなくても使用できる。Native pipeとも呼ばれる。
  • %>%
    • magrittrパッケージで提供されている。おそらく最初にRでパイプ演算子を導入したもの。
  • %>>%
    • pipeRパッケージで提供されている。現在ではあまり使われていないかもしれない。

以降では、|>%>%の機能面での違いについて紹介する。

実行速度

library(ggplot2)
library(magrittr)

# ネストさせる用の関数。第一引数をそのまま返す
f <- function(x) x

# ベンチマーク
benchmark <- bench::mark(
  # `%>%`のベンチマーク(1, 5, 10回ネスト)
  magrittr_nest01 = NULL %>% f(),
  magrittr_nest05 = NULL %>% f() %>% f() %>% f() %>% f() %>% f(),
  magrittr_nest10 = NULL %>% f() %>% f() %>% f() %>% f() %>% f() %>% f() %>% f() %>% f() %>% f() %>% f(),
  
  # `|>`のベンチマーク(1, 5, 10回ネスト)
  native_nest01   = NULL |> f(),
  native_nest05   = NULL |> f() |> f() |> f() |> f() |> f(),
  native_nest10   = NULL |> f() |> f() |> f() |> f() |> f() |> f() |> f() |> f() |> f() |> f(),
  
  # パイプなしのベンチマーク(10回ネスト)
  no_pipe_nest10  = f(f(f(f(f(f(f(f(f(f(NULL))))))))))
)
summary(benchmark)
# A tibble: 7 × 6
  expression           min   median `itr/sec` mem_alloc `gc/sec`
  <bch:expr>      <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
1 magrittr_nest01 819.91ns 943.08ns   961273.    5.15KB     96.1
2 magrittr_nest05   1.84µs   2.13µs   442390.    2.86KB     44.2
3 magrittr_nest10   3.03µs   3.44µs   279000.        0B     55.8
4 native_nest01    41.09ns 122.94ns  8464304.        0B      0  
5 native_nest05   532.95ns 615.02ns  1453842.        0B    145. 
6 native_nest10     1.31µs   1.48µs   639918.        0B     64.0
7 no_pipe_nest10    1.27µs   1.48µs   644494.        0B     64.5
Code
benchmark %>%
  plot() +
  scale_x_discrete(limits = rev) +
  bench::scale_y_bench_time(breaks = c(1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2)) +
  theme_linedraw()

Tip

|>の方が高速だが、多くの場合違いを感じられるほどではない。 %>%は回数の多いループの中で沢山使うのは避けたほうがいいかもしれない。

Place holder

パイプ演算子はLHSをRHSの第一引数に渡す。 しかし、第一引数以外に渡したいという場合も考えられる。

# `grepl()`の第二引数に渡すことで、c(TRUE, TRUE, FALSE)にしたい。
c("apple", "pineapple", "banana") %>% grepl("apple")
## Warning in grepl(., "apple"): argument 'pattern' has length > 1 and only the
## first element will be used
## [1] TRUE
c("apple", "pineapple", "banana") |> grepl("apple")
## Warning in grepl(c("apple", "pineapple", "banana"), "apple"): argument
## 'pattern' has length > 1 and only the first element will be used
## [1] TRUE

これはPlace holder(引数の位置を指定できる変数のようなもの)を使うと実現できる。 Place holderは%>%の場合は.|>の場合は_を使う。 また、|>の場合は_を名前付き引数に渡す必要がある。

# place holderは`.`。引数名を指定しなくても良い。
c("apple", "pineapple", "banana") %>% grepl("apple", .)
## [1]  TRUE  TRUE FALSE

# place holderは`_`。引数名を指定しなければいけない。
c("apple", "pineapple", "banana") |> grepl("apple", x = _)
## [1]  TRUE  TRUE FALSE
# 引数名を指定しないとエラーになる。
c("apple", "pineapple", "banana") |> grepl("apple", _)
Error: pipe placeholder can only be used as a named argument (<text>:2:38)
Tip

RHSの第一引数以外にLHSを渡す場合は、place holder (_または.)を使用する。 ただし、|>の場合(_を使う場合)は引数名を指定する必要がある。

引数名を指定できない場合はどうしたら良いだろうか? %>%は特に何も考えず、place holderを目的の位置引数に指定できる。

# `paste()`関数はいくらでも引数名なしで引数を受け取る。(`...`の部分)
paste
#> function (..., sep = " ", collapse = NULL, recycle0 = FALSE) 
#> .Internal(paste(list(...), sep, collapse, recycle0))
#> <bytecode: 0x1402705b0>
#> <environment: namespace:base>

# `%>%`は問題なく渡せる
"world" %>% paste("hello", .)
#> [1] "hello world"

一方で、|>は引数名なしではplace holderを使うことはできないのでエラーになる。。

# `|>`ではエラーになり実行できない。
"world" |> paste("hello", _)
Error: pipe placeholder can only be used as a named argument (<text>:2:12)

このようなとき、|>では無名関数を使うことで実現できる。 しかし、記述が多少煩雑になるため、可読性が低下するかもしれない。

# 引数の位置を指定する無名関数を書いて、その無名関数に渡す。
"world" |> (function(x) paste("hello", x))()
#> [1] "hello world"
"world" |> (\(x) paste("hello", x))()
#> [1] "hello world"

quote( "world" |> (\(x)paste("hello", x))() )
#> (function(x) paste("hello", x))("world")
Code
# 実はこの例だと、`|>`でも`x = _`のように適当な引数名をつけることで同じ挙動にできる。
c("world") |> paste("hello", x = _)
#> [1] "hello world"

Place holderの複数回使用

Place holderを複数回使用するなど、少し変わった使い方をする場合に|>%>%の柔軟性の違いが大きく現れる。

# `%>%`はそのまま複数回使うことができる。
"Hi!" %>% paste(., .)
#> [1] "Hi! Hi!"
# `|>`はエラーが出て実行できない。Place holderは一度しか指定することはできない。
"Hi!" |> paste(x = _, y = _)
Error: pipe placeholder may only appear once (<text>:2:10)
# 同じことを`|>`でも実現するには無名関数を利用する。
"Hi!" |> (\(x) paste(x, x))()
#> [1] "Hi! Hi!"
Tip

%>%は複数回place holder(.)を使用できる。

RHSが入れ子になった関数で、内側の関数にLHSを渡したい場合はどうすれば良いだろうか?

# 入れ子にする用の関数。第一引数に受け取った文字列を自分の関数名で包んで返す。第二引数以降は無視する。
outer <- function(x = "", ...) glue::glue("outer({x})")
inner <- function(x = "", ...) glue::glue("inner({x})")

# RHSが入れ子構造の関数になっている。
# 内側の関数`inner`にLHSを渡して、"outer(inner(hello))"と出力したいがそうならない。
"hello" |> outer(inner())
#> outer(hello)
"hello" %>% outer(inner())
#> outer(hello)

# これは実行されるときに以下のように、`outer`関数の第一引数にLHSが渡されているためである。
quote("hello" |> outer(inner()))
#> outer("hello", inner())

%>%では、RHSを{}で囲むことで任意の位置の引数に渡すことができる。 |>では、無名関数を使う必要がある。

"hello" %>% {outer(inner(.))}
#> outer(inner(hello))
"hello" |> (\(x) outer(inner(x)))()
#> outer(inner(hello))

まとめると、%>%を使う場合、RHSを{}で囲むだけでかなり柔軟にplace holder (.)を利用することができる。 一方で|>は制限が強いため、place holder (_)を柔軟に利用することは難しい。

Tip

%>%は入れ子や複数回place holderを使うときには、RHSを{}で囲むだけで良い。 |>は、place holderの利用に強い制限があるため、無名関数などを利用する必要がある。

右辺(RHS)の扱い

参考

Sessioninfo

sessionInfo()
R version 4.3.2 (2023-10-31)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.11.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Asia/Tokyo
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] magrittr_2.0.3 ggplot2_3.5.1 

loaded via a namespace (and not attached):
 [1] gtable_0.3.5      jsonlite_1.8.8    dplyr_1.1.4       compiler_4.3.2   
 [5] crayon_1.5.2      tidyselect_1.2.1  ggbeeswarm_0.7.2  tidyr_1.3.1      
 [9] scales_1.3.0      yaml_2.3.9        fastmap_1.1.1     R6_2.5.1         
[13] generics_0.1.3    knitr_1.48        htmlwidgets_1.6.4 tibble_3.2.1     
[17] munsell_0.5.1     pillar_1.9.0      rlang_1.1.4       utf8_1.2.4       
[21] xfun_0.46         cli_3.6.3         withr_3.0.0       digest_0.6.34    
[25] grid_4.3.2        beeswarm_0.4.0    lifecycle_1.0.4   vipor_0.4.7      
[29] vctrs_0.6.5       bench_1.1.3       evaluate_0.24.0   glue_1.7.0       
[33] farver_2.1.2      profmem_0.6.0     fansi_1.0.6       colorspace_2.1-1 
[37] rmarkdown_2.25    purrr_1.0.2       tools_4.3.2       pkgconfig_2.0.3  
[41] htmltools_0.5.7