読者です 読者をやめる 読者になる 読者になる

( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

「関数型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では中置演算子を独自に定義することはできないので、 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には必要ないのでは?