l'essentiel est invisible pour les yeux

Monday, November 13, 2006

[ruby] RubyでMap by Method

2年前PHPでガリガリと作って工夫をこらし考えていたときには、試行錯誤を繰り返していたが、Railsを使い出してからついついRails内の機能で満足してしまう。

よほど気をつけていないと、そこには改善すべき面白い機能がたくさんあるのにそれを見逃してしまう。多少面倒でもRuby&Railsの十分な恩恵にあずかっていて、それで十分簡単だと思い込んでしまう。

例えばSymbol#to_procを使用した次のようなコードをよく書く。


Person.find(:all).map(&:username)
Person.find(:all).map {|obj| [obj.username, obj.age]}

Symbol#to_procは、とても美しいく便利でよく利用する分だけ、このコーディングが冗長に思えてくる。

method_missingを利用してメソッドをmapメソッドと関連付けDynamic Map?(ARのDynamic Finderにちなんで勝手に命名)として作用させるというアイデアに出会った。

require 'rubygems'
require 'active_support'

class Rubyist
attr_accessor :name
attr_accessor :lang

def initialize(name, lang)
self.name = name
self.lang = lang
end
end

module MapByMethod
def self.included(base)
super

base.module_eval <<-EOS
def method_missing(method, *args, &block)
begin
super
rescue NoMethodError => e
if match = method.to_s.match(/(map|select|collect|each|reject)_([\\w\\_]+)/)
iterator, methods = match[1], match[2].split('_and_')
return self.send(iterator) {|item| methods.map {|method| item.send method}}
else
return self.map {|item| item.send method.to_s.singularize}
end
raise e
end
end
EOS
end
end
Array.send :include, MapByMethod

rubyists = [Rubyist.new("Matz", "ja"), Rubyist.new("DHH", "en"), Rubyist.new("takahashi", "ja"), Rubyist.new("moriq", "ja")]
p rubyists.names
p rubyists.map_name
p rubyists.map_name_and_lang


やっていることは単純で、method_missingを利用して高階関数をDynamicに適用するメソッドを利用可能にしている。

  1. 定義されていないメソッド(ある英単語の複数系)が与えられた際には、メソッド名を単数形にしたメソッドがmapへの高階関数として渡される。
  2. _and_でメソッド名をつなぐことにより、それぞれのメソッドを呼び出した結果を配列にして返す。

実行結果は次の通り。

p rubyists.names # => ["Matz", "DHH", "takahashi", "moriq"]
p rubyists.map_name # => [["Matz"], ["DHH"], ["takahashi"], ["moriq"]]
p rubyists.map_name_and_lang # => [["Matz", "ja"], ["DHH", "en"], ["takahashi", "ja"], ["moriq", "ja"]]
はい、美しいです。

参考
New magical version of Symbol.to_proc