[Pkg紹介]Base Pipeのplaceholder問題解決?pipebindパッケージを試す

BasePipe

はじめに

R界隈の皆様、こんにちはッ!!‍ 🙇‍♂️🙇‍🙇‍♂️

土曜の朝、数日ぶりにブログのアクセスログを見たら一日300人のユニークユーザー訪問という謎状況になってめちゃくちゃテンパった私です😵 (マジで不正ログイン攻撃だと思って焦った)

改めて自己紹介ですが、私は企業で生物系研究員🧬をしているヒトです。スニッチは猫からいただいた名で、主にソシャゲする時のアバター名です。

これを機にR界隈SNSに参入させていただきましたので、Twitterフォローよろしくお願いします🙇‍♂️

ぶっちゃけ最近はもっぱらpythonで仕事してますが、ひょんなことから社内でRレクチャーを実施することになり、きちんと基礎から学び直しているところです。このブログはその "レクチャーの補足的な意味合い" もあるため、かなり初心者向けの内容も多くなっております。

以後よろしくお願いします。

pipe bindという隠しコマンド

そもそもbase pipeについては先日の記事の時に初めて調査をしたので、去年R4.1.0系の状況をあまりよく知らなかったのですが・・・😓

base pipeとともにpipe bindという機能が検討されているそうです。

メーリングリストでの議論としては2021年1月が初出?かと思われます

@rdataberlinさんが2021年5月にこの件をtweetされてます。

=>をpipe bindと呼称するようです。デフォルトではdisabledされていますが、以下の環境変数を変更すれば使用可能になります。

注意

R4.2.0でテストしています。

Sys.setenv("_R_USE_PIPEBIND_" = "true")

iris |>
    x => lm(Sepal.Length ~ Petal.Length, data = x)
# Call:
# lm(formula = Sepal.Length ~ Petal.Length, data = x)

# Coefficients:
#  (Intercept)  Petal.Length  
#       4.3066        0.4089  

ラムダ式のような構文ですλ….

expr()を使ってシンタックスシュガーを解くと、まさにラムダ式そっくりな感じ。引数を後ろに書くことができるのですね。

dplyr::expr( iris |> x => lm(Sepal.Length ~ Petal.Length, data = x))
# (function(x) lm(Sepal.Length ~ Petal.Length, data = x))(iris)

ちょっと覚えにくい構文かな~と思いつつ、謎の近未来感があります。

pipe bindを使いやすくする{pipebind}パッケージ

そんなpipe bind機能を扱いやすい形でbind()関数として提供しているのが{pipebind}パッケージです。

GitHub - bwiernik/pipebind: Flexible binding for complex function evaluation with the Base R |> pipe
Flexible binding for complex function evaluation with the Base R |> pipe - GitHub - bwiernik/pipebind: Flexible binding for complex function evaluation with the...

導入方法

GitHubからインストールします。

remotes::install_github("bwiernik/pipebind")

使い方

bind()関数を使用し、まさにpipe bindとよく似た構文を使用します。

pacman::p_load(pipebind)

iris |>
    bind(x, lm(Sepal.Length ~ Petal.Length, data = x))
# Call:
# lm(formula = Sepal.Length ~ Petal.Length, data = x)

# Coefficients:
#  (Intercept)  Petal.Length  
#       4.3066        0.4089  

pipe bindのように、プレースホルダー的機能をラムダ式的な構文が担っているため、以前紹介したプレースホルダーの不便さがすべて解決します。

一応書いておきますが、ラムダ式的な使い方なので変数名は好きに指定できます。

注意

ただし、_はplaceholderとして評価されるため、意図した挙動にはなりません。

# OK
iris |>
    bind(hogehoge , lm(Sepal.Length ~ Petal.Length, data = hogehoge))
# OK
iris |>
    bind(. , lm(Sepal.Length ~ Petal.Length, data = .))
# BAD
iris |>
    bind(_ , lm(Sepal.Length ~ Petal.Length, data = _))
#  エラー: pipe placeholder can only be used as a named argument

pipebindは世界を救うか?

ここからはBase pipe調べてみたにてエラーになっていた|>とplaceholder構文がpipebind::bind()で解決するのか、一つずつ検証してみたいと思います🌟

引数名の指定問題⇒解決します

Base pipeのplaceholderには、引き数名を指定しなければならないというルールがありました。

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

pipebindのbind()関数ではこれが問題なく動作します。

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

[[]]を使いたい⇒使えます

Base pipeでは引数名問題とは別に、[[]]によるインデックスも使えませんでした。

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

これについても、pipebindで解決します。

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

$を使いたい⇒使えます

$についても同じです。

starwars |> 
    _$name |> 
    head()
#  エラー: pipe placeholder can only be used as a named argument
starwars |> 
    bind(x, x$name) |> 
    head()
# [1] "Luke Skywalker" "C-3PO"          "R2-D2"         
# [4] "Darth Vader"    "Leia Organa"    "Owen Lars"  

placeholderを二回以上使いたい⇒使えます

Twitterでのコメントをみると、皆さんのこれが一番気になるポイントのようでした。

繰り返しますが、Base pipeはplaceholderを二回以上使うことができません。前の記事にて追記もしましたが、base pipeの特性上placeholder位置にてパイプ以前の処理がすべて評価されるため、placeholderを置いた数だけ同じ処理を何度もする羽目になる、というのがこの設計の由来ではないかと考えられます。

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

bind()関数では以下のようにして使うことができます。もちろん、何回でも使用可能です。

1:10 %>%
    bind(x, rnorm(n = x, mean = x))
Info

xとしてパイプ以前の式を評価し終えた値が渡されるため、何度も同じ計算が行われる心配はありません。

前回記事でも述べたとおりtidyverse系パッケージ関数の"暗黙の第一引数"を含めて二回以上、なので実質tidyverse計パッケージの関数でplaceholderは使用不可です。

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

この件もpipebindのbind()で解決はしますが、第一引数も明示的に与える必要があります

# GOOD
starwars |>
    bind(x, mutate(x, newcolumn = nrow(x)))
# BAD
starwars |>
    bind(x, mutate(newcolumn = nrow(x)))

for文⇒できます

for文に関してはpurrr::map()を使いましょう、と提案しましたが、一応pipebindにより実行が可能になります。

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

結論:日常使いには申し分ないが、メイン使いは早すぎるか?

というわけで、今回紹介したとおりbase pipeで出来なかったことは全てpipebindで解決することが分かりました💡

完全にmagrittr pipeから卒業できる、という意味で大きな成果だと思います。

…が、本気で乗り換える気にはまだなれませんね

言うまでもないですが、base pipeもpipbindも今後仕様変更の可能性があります。

Base pipeは結構な議論の末に生まれたので「やっぱやめた!」ってことにはならないかもしれませんが、pipebindそのものがデフォルト採用される日がいつか来るかもしれません。

もしたくさんのコードでpipebindを使ってしまうと、再利用できないコードとして資産にならないかもしれません。。

Info

Rはコミュニティドリブンで驚異的な進化を遂げた経緯ゆえに、後方互換性の無い関数仕様は結構あるあるなのかなと思います。今回のpipebindなんかはまさに後方互換性が全く保証されていない、一種の提案手法である点には注意しましょう。

ということで、magrittrを完全卒業する手助けになる、pipebind::bind()の紹介でした! 私はターミナルで動かすちょっとした解析なんかで使ってみようと思っています! ではまた!

おまけ

Elephant at sunset
 

…🤔

pacman::p_load(pipebind)
assign("\u03bb", bind)
c(a = 1, b = 2, c = 3) |>
    λ(x, paste(names(x), x, sep = " "))
    
# [1] "a 1" "b 2" "c 3"
Elephant at sunset
 

🤣🤣🤣🤣

コメント

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