Ruby でクラス定義の DSL 作成

cuzic です。

今日も Ruby の話題です。

理由はないのですが、大林さんの昔出した課題のような問題に取り組んでみました。

目標は、

  A = AttrClass :accessor, :with_default => 1 do
    def initialize
      puts "initialized"
    end

    def do_something
      puts "do_something"
    end
  end
  a = A.new #=> intialized
  p a.accessor #=> nil
  a.accessor = "some value"
  p a.accessor #=> "some value"
  p a.with_default #=> 1
  a.with_default = 2
  p a.with_default #=> 2
  a.do_something #=> do_something

となるような AttrClass メソッドを定義することです。

つまり、AttrClass メソッドの呼び出しが下記のコードと等価になるようにする、ということです。

 class A
   attr_accessor :accessor
   attr_accessor :with_default 
   def initialize
     @with_default = 1
     puts "initialized"
   end

   def do_something
     puts "do_something"
   end
 end

これはやってみると、比較的簡単にできました。
次のようなメソッド定義になります。

def AttrClass *symbols, &block
  Class.new do |klass|
    symbols.each do |sym|
      case sym
      when Symbol
        attr_accessor sym
      when Hash
        klass.class_eval do
          sym.each do |key, value|
            attr_accessor key
          end
        end
        Module.new do
          new_original = klass.method :new
          define_method :new do |*args, &block|
            obj = new_original.call *args, &block
            sym.each do |key, value|
              obj.__send__ "#{key}=", value
            end
            obj
          end
          extend_object klass
        end
      end
    end
    klass.class_eval &block
    klass
  end
end

ポイントは、Class#class_eval を使って、Class のコンテキストでブロックを評価していることや、new クラスメソツドを再定義して、インスタンス変数の初期化を行っているところでしょうか。

これを使えば、アクセサなどが多いクラス定義がより簡潔に書けます。