Ruby で [-1, -2, -3].map(&:succ >> :to_s) という感じで書きたい


[-1, -2, -3].map(&:succ >> :to_s) という関数型を意識した記述

& を使いブロック渡しで任意のオブジェクトを渡す仕組みがあります。

カラクリについては https://qiita.com/hamajyotan/items/f2a96bbb1ccc60a06053 に記述。

[-1, -2, -3].map(&:succ)  #=> [0, -1, -2]

上記記述に対して、

Proc#>> と同じ要領で succ を実施した結果に更に to_s を実施できたら嬉しいですね。

けど、できません。。 :sweat_drops:

この記事はこれをできるようにするための実装の話です。

[-1, -2, -3].map(&:succ >> :to_s)  #=> NoMethodError: undefined method `>>' for :succ:Symbol


Proc#>> および Proc#<<

Ruby 2.6 から、 Proc#>> および Proc#<< が追加されました。いわゆる関数合成と言われる仕組みです。

f1 = -> x { x.succ }

f2 = -> x { x.to_s }

f3 = f1 >> f2
f3.call(-3) # f1 の実行結果を f2 の引数として渡す。 f2.call(f1.call(-3))
#=> "-2"

f4 = f1 << f2
f4.call(-3) # >> とは逆順に合成される。 f1.call(f2.call(-3))
#=> "-4"


Symbol#>> と Symbol#<< があったら嬉しい

自分自身を to_proc により Proc オブジェクトに変換、そこに更に >> で合成すればよさそうです。

# >> および << メソッドを生やす

module SymbolComposeToLeftAndRight
def >>(g)
to_proc >> g
end

def <<(g)
to_proc << g
end
end
Symbol.prepend(SymbolComposeToLeftAndRight)

# どうだ!?。。。残念
[-1, -2, -3].map(&:succ >> :to_s) #=> NoMethodError: undefined method `call' for :to_s:Symbol

どうやら Proc#>> の引数に対して call を要求しているようです。


Proc の >> および << の引数側も to_proc するようになれば嬉しい

Proc#>> および Proc#<< の引数は Proc オブジェクトであることが期待されています。

もう少し正確に言うと、 call に反応するオブジェクトであることが期待されています。

class Callable

def call(x)
x + 5
end
end

proc1 = proc { |x| x + 1 }
proc2 = proc1 << Callable.new

# 引数に 1加算され、更にその結果に 5加算。
proc2.call(3) #=> 9

ここが拡張されて、引数自体に to_proc がかかってから既存の処理となるようにならないでしょうか?

言い換えると、 .call を期待するオブジェクト改め、 .to_proc.call を期待するオブジェクトと言っても良いです。

module ProcComposeToLeftAndRightWithToProc

def >>(g)
super(g.to_proc) #=> 引数オブジェクトを to_proc してから既存の処理
end

def <<(g)
super(g.to_proc)
end
end
Proc.prepend(ProcComposeToLeftAndRightWithToProc)

これで上記エラーも回避できるはず。


実装した (まとめ)

module ProcComposeToLeftAndRightWithToProc

def >>(g)
super(g.to_proc)
end

def <<(g)
super(g.to_proc)
end
end
Proc.prepend(ProcComposeToLeftAndRightWithToProc)

module SymbolComposeToLeftAndRight
def >>(g)
to_proc >> g
end

def <<(g)
to_proc << g
end
end
Symbol.prepend(SymbolComposeToLeftAndRight)

# succ してから to_s
[-1, -2, -3].map(&:succ >> :to_s) #=> ["0", "-1", "-2"]

# succ してから to_s, さらに length
[-1, -2, -3].map(&:succ >> :to_s >> :length) #=> [1, 2, 2]

# 逆側の合成。 to_s してから succ
[-1, -2, -3].map(&:succ << :to_s) #=> ["-2", "-3", "-4"]

関数型っぽい!

既存 2.6 とも互換はあるし、 Ruby の自体がこうなってたら嬉しい人は少なくないんじゃないかと思うけどどうでしょう?