Enumerable#select と #collect を同時に行いたい

せっかくなので小ネタを。

Python でのリスト内包表記は、for と if を同時に書ける。これはループを1回まわるだけで、選択 (select) と写像 (collect) を一度に行えることを意味する。

>>> L = ['foo', 'bar', 'baz']
>>> [ x.upper() for x in L if x.startswith('ba') ]
['BAR', 'BAZ']

これと同じことを Ruby でやると、Enumerable#collect と #select を使うわけだが、そうするとループを2回まわることになり、アルゴリズム的には動作効率はよくない。

irb> arr = ['foo', 'bar', 'baz']
=> ["foo", "bar", "baz"]
irb> arr.select {|x| x =~ /\Aba/ }.collect {|x| x.upcase }
=> ["BAR", "BAZ"]

ところで最近知ったのだが、 Enumerable#grep はブロックを取れるらしく、そのブロックはちょうど Enumerable#collect のような動作を行うらしい。
というわけで、select と collect を1回のループで同時に行うには grep を使えばいいみたい。

irb> arr = ['foo', 'bar', 'baz']
=> ["foo", "bar", "baz"]
irb> arr.grep(/\Aba/) {|x| x.upcase }
=> ["BAR", "BAZ"]

これはつまり、select {|x| x =~ /\Aba/ } を grep(/\Aba/) で代用したわけだが、残念ながら grep() は select {} ほど融通はきかない。grep() の引数は '===' 演算子をサポートしたものしか指定できないので、ブロックを指定できる select と比べれば、明らかに自由度は低い。

そこで、Proc に '===' 演算子を定義してみる。中身は、単に call するだけ。

class Proc
  def ===(arg)
    self.call(arg)
  end
end

## 使い方
proc_obj = proc {|x| p x }
proc_obj === 'FOO'    #=> "FOO"

これを利用すると、任意の処理を grep() の引数に渡すことができるため、grep() で Enumerable#select 相当が可能になる。つまり、1回のループで collect と select を同時に行うことが可能になる。

irb> arr = ['foo', 'bar', 'baz']
=> ["foo", "bar", "baz"]
irb> arr.grep(proc {|x| x =~/\Aba/ }) {|x| x.upcase }
=> ["BAR", "BAZ"]

正規表現ではできない例をやってみる。

irb> arr = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
irb> arr.grep(proc {|x| x % 2 == 1 }) {|x| x*x }
=> [1, 9, 25]

なかなかよろしいんじゃないでしょうか。

というわけで、Proc#=== の導入を提案したい。もし Ruby 本体はだめでも、Rails なら、きっと Rails ならなんとかしてくれる!

# '===' 演算子が使われている箇所って、case 文と grep() しか知らないけど、他にあるのかな。