defもclassもmoduleも使わないrubyメタプログラミング

rubyリングにも参加したことでもあるし、今日はrubyネタ。
Pythonでのメタクラス・プログラミング」(http://www-06.ibm.com/jp/developerworks/linux/030425/j_l-pymeta.html)を読んで、ふと「Rubyでメタっぽいことやったらどこまで出来るかなあ」と思ったので、いろいろ試してみた。結局、文字列をevalすることもなく、def構文もclass構文もmodule構文も使うことなく、動的にクラスを生成できるところまで行き着いたのでご紹介。

メタなしかけの説明は後にして、まずはサンプル。こんなコードが書けるようになる。

#クラス定義
Def_class.call(
               "Counter",nil,
               proc {attr_reader :count},
               {
                 :initialize=>proc {|count|@count = count},
                 :inc=> proc {@count += 1}
               },
               {})

#class Counter
#  attr_reader :count
#  def initialize(init)
#    @count = init
#  end
#  def inc
#    @count +=1
#  end
#end


c = Counter.new(10)
c.inc
p c.count #=>11
c.inc
p c.count #=>12

Def_class.call以下の式一発でひとつのクラスを定義している。その下にコメントアウトしてあるのが、Def_classで定義しているクラスを普通に書いたもの。まあ、簡単なカウンターです。一番下のがサンプルコードと結果。クラス利用するOOPなコードを、defもclassもmodule構文も使わずに書けている。正直やりすぎだ。
Def_classには、

  1. クラス名
  2. 継承するクラス (nilだとObjectを継承)
  3. クラスのコンテキストで実行したいコード(Procオブジェクトで与える)
  4. インスタンスメソッドのハッシュ (メソッド名 => メソッドのProcオブジェクト)
  5. 特異メソッドのハッシュ (メソッド名 => メソッドのProcオブジェクト)

を順番に指定する。要は、普通のclass構文でのクラス定義をひとつのメソッド*1に押しこめてある。メソッド定義はProcオブジェクトで肩代わり。ちなみに無名クラスを作るDef_anonymous_classってのもあって、これは第一引数がないだけで後は同じ。
class構文を使ったクラス定義とDef_classとでは、書き方がちょっと違うだけに見える。まあ、普通にRuby使ってるだけだと、あんまり嬉しくないかもしれない。Def_classが嬉しいのはクラスを動的に定義したい時や、ほとんど同じだけどちょっとだけ違うクラスをたくさん必要とする時だ。
例えば、上のサンプルのように1づつ増えるカウンターの他に、10づつ増えるカウンターが欲しくなったとする。その時に、

class Counter10
  attr_reader :count
  def initialize(init)
    @count = init
  end
  def inc
    @count +=10
  end
end

なんてクラスを定義してしまうのは、あまりにも芸がない。Counterクラスと比べてもincメソッドでの増分しか違わないのに、似たようなクラスを作るのは面倒だし、なにより後からメソッドを追加しようとした時に、CounterとCounter10と両方のクラスをメンテしなきゃいけない。なんて恐しい。じゃあ、っていうんで「増分」をパラメータにしてみる。こんなかんじ。

class Counter_n
  attr_reader :count
  def initialize(init,step)
    @count = init
    @step = step
  end
  def inc
    @count +=@step
  end
end

newに与えるふたつ目の引数に増分が指定できるので、Counter_n.new(0,1)やらCounter_n.new(0,10)やらすれば、1や10づつ増えるカウンターができる。でも、最初のCounterと仕様が変わってしまうし、これだけ単純なクラスならいいけど、パラメータの数が増えるとそれだけnewに与える引数が増えてしまう。そこで、Def_classの登場。

def make_counter_class(step) #無名クラスを生成するメソッド
  return Def_anonymous_class.call(nil,
                                  proc {attr_reader :count},
                                  {
                                    :initialize=>proc {|count|@count = count},
                                    :inc=> proc {@count += step}
                                  },
                                  {})
end

#サンプルコード
Counter2 = make_counter(2) #クラス生成
c = Counter2.new(10)
c.inc
p c.count #=>12
c.inc
p c.count #=>14

Counter10 = make_counter(10) #クラス生成
c = Counter10.new(10)
c.inc
p c.count #=>20
c.inc
p c.count #=>30

Def_anonymous_classを使って無名クラスを動的に生成して返すメソッド、make_counter_classが定義できる。make_counter_classのに数字を与えると、その分だけ増えるカウンタークラスが生成される。これだとクラスを生成するのも簡単だし、定数に代入してやってクラスに名前をつければ、元のCounterクラスと全く同じように使えて、しかもコードはひとつだけなのでメンテもしやすい。
「クラスを動的に生成するメソッド」をclass構文を使って書こうとすると、メソッド定義の中にclass構文は書けないのでアウト。Class.new {}を使えば書けるが、defを使うとそこで変数のスコープが切れてしまうので、上の例のようにstepをパラメータ化することはできない。Counterクラス定義のソース文字列(stepの値だけ変数で埋めこみ)をevalしてやればstepのパラメータ化とクラス動的生成も可能だが、ソースの中に普通のコードとクラスを定義する文字列がまじるので、ちょっと嫌だ。この例では、Def_anonymous_classがただのProcオブジェクトなのでメソッド定義の中で使えるし、メソッド定義がProcオブジェクトなのでクロージャーの変数スコープの特性をつかって簡単にパラメータ化できる。

さて、では、Def_classやDef_anonymous_classの定義は、というと、これ。こちらにも、classもmoduleもdefもない。

proc {
  anonymous_factory = proc { |klass|
    return proc { |parent_klass,class_proc,method_hash,s_method_hash|
      parent_klass ||= Object
      newclass  = klass.new(parent_klass,&class_proc)

      #define instance method
      method_hash.each do |name,body|
        newclass.send(:define_method,name,&body)
      end
      newclass.instance_eval {public *(method_hash.keys)}

      #define singleton method
      tmodule = Module.new
      s_method_hash.each do |name,body|
        tmodule.send(:define_method,name,&body)
      end
      tmodule.instance_eval  {public *(s_method_hash.keys)}
      newclass.extend(tmodule)

      return newclass
    }
  }

  factory = proc {|klass|
    return proc { |*args|
      name,*subargs = args
      target = anonymous_factory.call(klass).call(*subargs)
      Object.const_set(name,target)
    }
  }

  Def_anonymous_class = anonymous_factory.call(Class)
  Def_anonymous_module = anonymous_factory.call(Module)
  Def_class = factory.call(Class)
  Def_module = factory.call(Module)

}.call

Procオブジェクトを多用して、妙に高階プログラミングになっているのでわかりにくいかもしれないが、面倒なので詳しい説明は省略。あ、ちなみにメソッドは全部publicになるようにしてある。結局は、一番下の部分で4つの定数にProcオブジェクトを代入するためだけのコードだ。これを保存したファイルをrequireなりで呼びだしておけば、最初のサンプルコードが動く。

で。ここまでひっぱってきてナンだけど、実はDef_classではeachやmapみたいな「ブロック付きメソッド」が定義できない。(先にも書いたように)Def_classの中ではメソッド定義にProcオブジェクトを使っているが、Procオブジェクトは(多分)ブロックを付けて呼びだすことができない。なので、ブロックを付けて呼びだされるメソッドは、Def_classでは定義できない。残念。もうひとつ、メソッドの引数にデフォルト値を設定することもできない。残念残念。あー、何か方法はないかなあ。ブロック付きメソッドがProcオブジェクトで定義できれば、大抵のクラスをDef_classで定義できるんだけどなあ。ま、自分でブロック付きメソッドを定義することは少ない気もするので、これでよしとするか。

*1:正確にはProcオブジェクト