はじめに
2019/6/13、Ruby 2.7にpipeline operatorが入りました。
discussionが盛り上がっててTLでも各Rubyist達の様々な意見が飛び交ってます。
そんな中、@hanachin_さんのツイートを見かけました。
よすぎて何回も眺めている
— Miyagi (@hanachin_) 2019年6月14日
"https://t.co/8sUOXpGLwt".:itself
|>>> URI.:parse
|>>> Net::HTTP.:get
|>>> JSON.:parse
|> call
|> fetch("stargazers_count")
|> then { puts @1 }
Ruby 2.7から追加される機能が積極的に使われていてすごく勉強になりそうだったので、まとめてみました。
認識間違っている・不足している箇所もあるかもしれないのでその際は教えてください。
何分はじめてのQiita記事なのでお手柔らかにお願いします。
Rubyのバージョン
2.7.0-dev
※2019/6/4時点でまだ2.7は開発中なので、2.7で新しく追加される機能の挙動は今後この記事とは変わってくる可能性があります。
本編
このコードは何をしているの?
まず@hanachin_さんのコードと下記のコードは等価です。
puts JSON.parse(Net::HTTP.get(URI.parse("https://api.github.com/repos/ruby/ruby"))).fetch("stargazers_count")
URLにGETでリクエストしてJSONを取得、そこからstargazers_count
を取得して出力する、という処理です。
このコードを読むために必要な知識
従来からある機能
Object#itself
Method#>>
Object#then
2.7の新機能
.:
- pipeline operator
- Numbered parameters
1行目
"https://api.github.com/repos/ruby/ruby".:itself
Object#itself
self
、つまりレシーバ自身を返します。
ここでは"https://api.github.com/repos/ruby/ruby"
です。
.:fuga
method
メソッドのショートハンドです。
hoge
のfuga
というMethod
オブジェクトが欲しい場合は
hoge.:fuga
と書けます。
1行目と等価なコード
"https://api.github.com/repos/ruby/ruby".method(:itself)
2行目〜5行目
|>>> URI.:parse
|>>> Net::HTTP.:get
|>>> JSON.:parse
|> call
pipeline operator
|>
のことです。
メソッドの後の()
が省略できる以外は.
でメソッド呼び出すのと同じです。
Method#>>
>>
の場合は左辺のMethod
オブジェクトの戻り値を引数として右辺を実行します。
<<
の場合は右辺の戻り値を引数として左辺のMethod
オブジェクトを実行します。
なので2行目〜5行目のコードは下記のようなイメージです。
|>>> URI.:parse # URI.parse(1行目の戻り値)を実行
|>>> Net::HTTP.:get # Net::HTTP.get(URI.parseの戻り値)を実行
|>>> JSON.:parse # JSON.parse(Net::HTTP.getの戻り値)を実行
参考:Ruby 2.6 の変更点 - Method と Proc
5行目までと等価なコード
-> {
JSON.parse(Net::HTTP.get(URI.parse("https://api.github.com/repos/ruby/ruby")))
}.call
6行目
|> fetch("stargazers_count")
Hash#fetch
で5行目までで取得したJSONのstargazers_count
プロパティを取得しています。
6行目までと等価なコード
JSON.parse(Net::HTTP.get(URI.parse("https://api.github.com/repos/ruby/ruby"))).fetch("stargazers_count")
7行目
|> then { puts @1 }
Object#then
self
をブロック引数に渡してブロックを評価し、ブロックの評価結果を返します。
ここでは6行目で取得したJSONのプロパティstargazers_count
がself
となります。
参考:Object#then
Numbered parameters
ブロック引数の定義を省略して@1
、@2
、@3
... に多重代入できます。
ここでは@1
には6行目の戻り値が入ってきます。
参考:Ruby 2.7 の Numbered parameters を試す
最終的な等価なコード
puts JSON.parse(Net::HTTP.get(URI.parse("https://api.github.com/repos/ruby/ruby"))).fetch("stargazers_count")
Ruby 2.7での追加機能超便利!
Ruby 2.7での追加機能、使いこなせれば気持ちよくコードが書けそうですね!
勉強にもなったしめでたしめでたし!・・・ではなかった
演算子がNG
このコミットでpipeline operatorの後に+
や-
等の演算子を使えなくなりました。
使えなくなった演算子一覧はここのコードで確認できます。(op
にあたる演算子が使えなくなりました)
※なぜpipeline operationの後に演算子がきてはダメなのか、理由はわからなかったのでQuoraで質問してみてるので返答くれば追記します。
2019/6/15 追記
@yukihiro_matzから返答いただけました!
Ruby2.7のpipeline operatorで+や-等の演算子をはじいたのはなぜなんでしょうか?
読みやすくない、むしろ読みにくいというのが主な理由とのことでした。
その結果
こうなりました。
ふつうにこれで動くしよいのではという気持ちになってきた
— Miyagi (@hanachin_) 2019年6月14日
"https://t.co/8sUOXpGLwt".:itself >> URI.:parse >> Net::HTTP.:get >> JSON.:parse
|> call
|> fetch "stargazers_count"
|> then { puts @1
}
pipeline operatorは()でラップしている?
上記のコードでcall
を.
で呼び出すと思い通りの挙動になりません。
関数合成した結果ではなくJSON.:parse
をレシーバにしてcall
を呼び出してしまうからです。
一方、pipeline operatorだと思い通りの挙動をするので、下記のようにレシーバを()
で囲って評価されてるのかな?という気がしてます。(コード読んだわけではないので確証はないです)
("https://api.github.com/repos/ruby/ruby".:itself >> URI.:parse >> Net::HTTP.:get >> JSON.:parse).call
最後に
pipeline operatorは賛否両論ありますが、Rubyistが幸せになれる結果になるといいですね
また、@hanachin_さんのコードのおかげで非常に勉強になりました、@hanachin_さんありがとうございます