背景
昨日新しいバージョンで今まで面倒だったことがとても簡単になった、というような気付きがあったので、
昔気づいた「こんなことが出来るようになったのか」な内容と、そこから考えてみたことを記事にしてみようと思います。
mapで変換しながらリストを組み立てるとき、単純に複数のメソッドを呼ぶだけってとき多くないでしょうか?
例えば、複数のオブジェクトのとある文字列プロパティーを取得したくて、
Vo = Struct.new(:prop1)
ary = [Vo.new("ab"), Vo.new("abc"), Vo.new("abcd")]
のようなaryから
["ab", "abc", "abcd"]
が欲しいんだけども、さらにそのそれぞれを大文字にした、
["AB", "ABC", "ABCD"]
が欲しい場合です。
問題
泥臭くすれば、
p ary.map {|e|
e.prop1
}.map {|e|
e.upcase
} #=> ["AB", "ABC", "ABCD"]
とmapを2回つなげれば実現できます。でも、これだと冗長ですよね。
で、1.9からシンボルに「&」を前置すると、Symbol#to_procが呼ばれてto_procした結果をブロックとして
渡せるようになりました。(1.8.7へはバックポートされた)
なので、上記の例は下記のように書けるようになりました。
p ary.map(&:prop1).map(&:upcase)
でも、これだと、さらに、反転(reverse)したリストが欲しい場合
p ary.map(&:prop1).map(&:upcase).map(&:reverse)
と、どんどんmapが重なって冗長になってきます。
「prop1で取得したプロパティーにupcaseした上で、reverseしたもののリストが欲しい」
というようなことが素直に出来ると嬉しいなぁとおもって考えたことがありました。
試してみたこと
symbolからto_procで作られる仕組みは、
to_procで、引数で渡されたreceiverに対して、sendで自分自身(Symbol)を渡す、という感じです。
class Symbol
def to_proc
lambda{|obj, *args|
obj.send(self, *args)
}
end
end
みたいな感じですね。
なので、symbolやらprocやらを渡された時に、さらにこれらを包むようにlambda(またはproc)を作ってやれば、
メソッドの連鎖が実現できそうです。
ということで、
module ComposeUtil
def >>(m)
lambda do |obj, *args|
m.to_proc.call(self.to_proc.call(obj, *args))
end
end
end
class Proc
include ComposeUtil
end
class Symbol
include ComposeUtil
end
というような>>
メソッドを定義すれば、procが渡されても、symbolが渡されても、どんどん連鎖できます。
(普通、合成関数の表記法の f○g の動きは、
f(g(x)
ですが、左から右に向かって進むようにみえるように f >> g
を
g(f(x))
というような動きになるようにしています)
これで、p ary.map(&:prop1).map(&:upcase).map(&:reverse)
と同様のことが、
p ary.map(&:prop1 >> :upcase >> :reverse)
で実現できます。
例えば、ここから、
prop1で取得したプロパティを大文字(upcase)にして反転(reverse)したものの最初の一文字(chars, first)の次の文字(succ)を2つ連結させたような値(これは無いのでlambdaを渡す)が欲しい場合
Vo = Struct.new(:prop1)
ary = [Vo.new("ab"), Vo.new("abc"), Vo.new("abcd")]
#普通に書いた場合
p ary.map(&:prop1).map(&:upcase).map(&:reverse).map(&:chars).map(&:first).map(&:succ).map {|x| x * 2}
#=>["CC", "DD", "EE"]
#>>で連鎖させた場合
p ary.map(&:prop1 >> :upcase >> :reverse >> :chars >> :first >> :succ >> ->(x){x * 2})
#=>["CC", "DD", "EE"]
と書けたりします。