LoginSignup
48
42

More than 5 years have passed since last update.

[Ruby]mapで複数のメソッドチェーンを渡したい

Posted at

背景

昨日新しいバージョンで今まで面倒だったことがとても簡単になった、というような気付きがあったので、
昔気づいた「こんなことが出来るようになったのか」な内容と、そこから考えてみたことを記事にしてみようと思います。

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"]

と書けたりします。

48
42
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
42