「関数型Ruby」という病(7) - Elixir's Pipe operator |> in Ruby
最近Elixirが人気ですよね。Erlang VM上でOTPの恩恵を受けながら簡潔な記法で並行処理を書ける言語ということで話題になっていますな? Elixirは関数型プログラミングのエッセンスを取り入れていると言われており、そのひとつにPipe演算子(|>) がある。
Pipe演算子(|>)とは何かというと、左辺の値を右辺の関数の第1引数に適用する演算子。
iex> [1, [2], 3] |> List.flatten() [1, 2, 3]
上記のコードは、左辺の[1, [2], 3]
を 右辺の List.fatten(list)
の引数として渡す。
このPipe演算子は、Stream
モジュールなどと合わせて利用するとデータが左から右へ流れている模様をコードとし視覚化することができるという利点があるっぽい(感じろ)。
iex(16)> f = fn a -> IO.puts "f(#{a}) : #{a+1}"; a ; end #Function<6.90072148/1 in :erl_eval.expr/5> iex(17)> g = fn a -> IO.puts "g(#{a}) : #{a*2}"; a * 2 ; end #Function<6.90072148/1 in :erl_eval.expr/5> iex(18)> 1..10 |> Stream.map(f) |> Stream.map(g) |> Enum.take(3) f(1) : 2 g(1) : 2 f(2) : 3 g(2) : 4 f(3) : 4 g(3) : 6 [2, 4, 6]
1..10 |> Stream.map(f) |> Stream.map(g) |> Enum.take(3)
というコードで、1から10のStreamに対してlazyに関数fとgを順番に適用しながら3つの要素を取り出すという様を素直に表現できていますね?(思え)
さて、そんな便利なパイプ演算子ですが、実は2年ほど前に作ったlambda_driver.gemに既に実装されていたりする。
- Rubyで関数合成とかしたいので lambda_driver.gem というのを作った - ( ꒪⌓꒪) ゆるよろ日記
- lambda_driver/revapply.rb at master · yuroyoro/lambda_driver · GitHub
Rubyでは中置演算子を独自に定義することはできないので、 Object
クラスに |>
というメソッドを生やすことで実現しよう(全角ェ)。
素朴な実装はこうだ
class Object def |>(f = nil) puts f if block_given? yield self else f.call(self) end end alias_method "|>", :>= end
さて、この全角の |>
を使って、上記のElixirのコードをRubyで書いてみるとどうなるか?
irb(main):059:0> f = ->(a){ puts "f(#{a}) : #{a + 1}" ; a + 1} => #<Proc:0x007ffb8d8b2348@(irb):59 (lambda)> irb(main):060:0> g = ->(a){ puts "g(#{a}) : #{a *2}" ; a * 2} => #<Proc:0x007ffb8d860a98@(irb):60 (lambda)> irb(main):061:0> (1..10).|>(&:lazy).|>{|x| x.map(&f) }.|>{|x| x.map(&g) }.|>{|x| x.take(3)}.|>(&:force) f(1) : 2 g(2) : 4 f(2) : 3 g(3) : 6 f(3) : 4 g(4) : 8 => [4, 6, 8]
なんというか、すごく……ダサいですね……。
というか、Rubyだったら素直にこう書いた方がいい
irb(main):143:0> (1..10).lazy.map(&f).map(&g).take(3).force f(1) : 2 g(2) : 4 f(2) : 3 g(3) : 6 f(3) : 4 g(4) : 8 => [4, 6, 8]
ごくごくまれに、左辺値がObjectとかでrevapplyを使いたくなることもなきにしもあらずだが、そういう場合でも大抵は Object#try
で事足りる。
結論 : Rubyには必要ないのでは?
Elixir、 |>の右辺が2引数以上の関数のときに、左辺が第一引数に埋め込まれるのが違和感。 1..10 |> Enum.map(&(&1 * 3)) だと、Enumの第一引数はlistなのにぱっと見で関数渡しているように見えてしまうのが気になる
— null (@yuroyoro) October 28, 2015
「関数合成は|>で代用できるよ!」って言ってるけどそうじゃないんだ。合成した後の関数をその場で適用するのではなく値として持ち回りたいんだよ…。あと|>につなげる関数をカリー化させたいんだよ
— null (@yuroyoro) August 26, 2015
Elixirのパイプライン(|>)ってようはOcamlのrevapplyってことか。これがあるのに関数合成やカリー化がないのは本当に惜しい
— null (@yuroyoro) August 26, 2015
カジュアルに関数がカリー化、部分適用できれば|>がもっと輝くのに…
— null (@yuroyoro) October 28, 2015