2012年7月26日木曜日

Rubyでフック処理

ここ2日ほどフック処理をするためのモジュールを作ろうとして四苦八苦していた。あちこち検索していろんなソースを参考にしつつ、とりあえずこんな感じになった。

module Hooker
  def self.included(klass)
    klass.extend(ClassMethods)
    klass.__send__ :include, InstanceMethods
  end
  module ClassMethods
    def define_hook(name, &hook)
      define_method(name) do |*args, &block|
        singleton_class =(class << self; self; end)
        singleton_class.module_eval{define_method(:__instance_exec, &hook)}
        orig = proc{|*args, &block| super(*args, &block)}
        begin
          return send(:__instance_exec, orig, *args, &block)
        ensure
          singleton_class.module_eval{remove_method(:__instance_exec)} rescue nil
        end
      end
    end
  end
  module InstanceMethods
  end
end

どういうときに使うかというと、例えばArrayクラスを継承したMyArrayクラスを定義して、MyArray#mapの戻り値をArrayではなくMyArrayで受け取りたい、というような場合に使う。普通にクラスを継承させただけでは、MyArray#mapの戻り値はArrayオブジェクトになる。

class MyArray < Array
end

ary = MyArray.new([1,2,3])
new_ary = ary.map{|n| n + 1}
puts new_ary.class #=> Array

Hookerモジュールを使うと、次のように#mapの戻り値をMyArrayオブジェクトで返すMyArrayクラスを定義することができる。

class MyArray < Array
  include Hooker
  define_hook(:map){|orig, &block| self.class.new(orig.call(&block))}
end

ary = MyArray.new([1,2,3])
new_ary = ary.map{|n| n + 1}
puts new_ary.class #=> MyArray

おおまかな使い方としては、define_hookメソッドでフックしたいメソッドをオーバーライドする。define_hookの引数はフックしたいメソッドの名前で、ブロックはフック処理である。ブロックの第一引数origはsuperをProc化したものである(定義ブロック内ではsuperが使えないのでこのような方法を取っている)。ブロックの第二引数以降では*argsや&blockなど、メソッドに渡したい引数を与えることができる。引数を加工した上で元メソッドに引き渡したい時は、argsやblockを加工した上でorig.call(*args, &block)などとする。メソッドの戻り値を加工した上で取得したい時は、orig.callの戻り値を加工する。上記では、mapの戻り値を加工してArrayからMyArrayに書き換えている。

別の使い方としては、eachメソッドをフックして#eachのブロックの引数を加工することもできる。

class MyArray < Array
  include Hooker
  define_hook(:each){|orig, &block| orig.call{|x| block.call(self.class.new(x))}}
end

ary = Bar.new([[1,2,3], [4,5,6], [7,8,9]])
ary.each{|x| puts "#{x}:#{x.class}"}

#=>
[1, 2, 3]:MyArray
[4, 5, 6]:MyArray
[7, 8, 9]:MyArray

手こずったポイントの一つは、define_hookに渡す定義ブロックが、クラスのコンテキストで生成されてしまうということ。このため、定義ブロック内でselfを使うとインスタンス(=ary)ではなくクラス(=MyArray)を参照してしまうという問題が生じた。そこでinstance_execを使って定義ブロック(=&hook)を強制的にインスタンスのコンテキストで実行することを考えたが、instance_execは&hookに引数を引き渡すことはできるものの、ブロックの引渡し方がよく分からない。このため、eachなどブロックを取るメソッドをフックしたいときに、このブロックをどうやって&hookに引き渡せばいいかで悩むことになった。最終的に、instance_execのソースを探してきて、似たようなコードを自前で書くことによって問題を解決した。あとはsuperが使えるようになれば、もっと直感的な使い勝手になると思うのだが・・・。

2012年7月23日月曜日

RubyのDelegatorを使ってみる話

例えば、Arrayクラスにhelloというメソッドを追加したい場合、次のようなやり方ができる。

class Array
  def hello
    puts "Hello world !!"
  end
end

しかし、もともとあるArrayクラスに手を加えたくない場合もある。いくつかやり方があるが、Arrayクラスを継承した別のクラスを定義するという方法がある。

class HelloArray < Array
  def hello
    puts "Hello world !!"
  end
end

しかし、HelloArrayクラスのオブジェクトにmapなどのメソッドを使うと、Arrayクラスに戻ってしまう。

obj1 = HelloArray.new([1, 2, 3])
obj2 = obj1.map{|n| n + 1}
obj2.class # => Array

そこで、obj1に何か仕事をさせて、戻り値がArrayであれば自動的にHelloArrayにするという処理を実現したいとする。これはdelegateという標準ライブラリを使って次のように実現することができる。

require 'delegate'
class HelloArray < SimpleDelegator
  def method_missing(name, *args, &block)
    obj = super(name, *args, &block)
    obj = HelloArray.new(obj) if obj.class == Array
    return obj
  end
end

obj1 = HelloArray.new([1, 2, 3])
obj2 = obj1.map{|n| n + 1}
obj2.class # => HelloArray

SimpleDelegatorはmethod_missingを利用してオブジェクトへの委譲を行う。そこで、SimpleDelegatorを継承したHelloArrayクラスを定義して、method_missingを書き換えることによって「ほとんど全てのメソッドを対象としたフック処理」を行うことができる。これにより、メソッドの戻り値がArrayかどうかを監視して、ArrayであればHelloArrayにするという処理を実現することができる。

これで、obj1によって生成されるArrayオブジェクトは全て自動的にHelloArrayオブジェクトになるようになったはずである。ところが、次のような処理はうまく行かない。

obj1 = HelloArray.new([[1,2,3],[4,5,6],[7,8,9]])
obj1.each{|item| item.hello}

itemには[1,2,3]や[4,5,6]や[7,8,9]が順番に代入されるが、これらはobj1のメソッドの結果として生成されるわけではないので、HelloArrayの監視対象ではない。そこでHelloArrayのeachメソッドを上書きして、これらの要素も監視するようにしよう。

class HelloArray
  def each
    super do |item|
      item = HelloArray.new(item) if item.class == Array
      yield item
    end
  end
end

これで、obj1.each{|item| ... }のitemも、自動的にArrayからHelloArrayに変換されるようになる。当然、eachメソッドを参照するEnumerable系のメソッド(mapとか)にも自動的に適用される・・・と思いきや、実はうまく行かない。

obj1.map{|item| item.hello} # => Error!

HelloArrayクラスにはmapは定義されていないので、method_missingを経由してArrayに委譲されることになる。ところが、Array#mapはあくまでArray#eachを参照するのであって、HelloArray#eachを再定義しても無視されるのではないかと思われる。ならばどうするのかというと、HelloArrayクラスにEnumerableをインクルードしてやる。

class HelloArray
  include Enumerable
end

これで、HelloArray#mapその他が定義され、これらはHelloArray#eachを参照するので、mapも期待通り動作するようになる。が、今度はEnumerableがインクルードされてHelloArray#mapが定義されたことにより、method_missing経由でArray#mapが呼び出されなくなるので、method_missingで行なっていたmapの結果をHelloArrayに変換する処理が行われなくなってしまう。これを解決するには、デリゲーターを二重構造にするという方法がある。

class HelloArray < SimpleDelegator
  def hello
    puts "Hello World !!"
  end
  def method_missing(name, *args, &block)
    obj = super(name, *args, &block)
    obj = HelloArray.new(obj) if obj.class == Array
    return obj
  end
end

class HelloArrayInner < SimpleDelegator
  include Enumerable
  def each
    super do |item|
      item = HelloArray.new(item) if item.class == Array
      yield item
    end
  end
end

obj1 = HelloArray.new([])
obj2 = HelloArrayInner.new(obj1)

これで、obj2#mapのブロック引数はHelloArrayになるし、戻り値もHelloArrayになる。最後に、obj1、obj2と二段階に分けてデリゲーターを生成するのは手間なので、一回ですむようにHelloArray#initializeを修正することにしよう。

class HelloArray
  def initialize(obj)
    super
    self.__setobj__ HelloArrayInner.new(obj)
  end
end

Delegator#__setobj__は委譲先オブジェクトを変更するメソッドである。ここではobjからHelloArrayInnner.new(obj)へと委譲先を差し替えることで、HelloArrayとobjの間にHelloArrayInnerをはさみこんでいる。これにより、次のような書き方で二重構造のデリゲーターを生成することができるようになる。

obj = HelloArray.new([])

2012年7月13日金曜日

正規表現とチョムスキー階層に関する簡単なまとめ

テキスト検索などに利用される正規表現は、チョムスキーのタイプ-3文法(正規文法)に由来する。チョムスキーは句構造文法をタイプ-0からタイプ-3までの4つの階層に分類した。

タイプ-0-制限なし
タイプ-1文脈依存文法αAβ→αγβ
タイプ-2文脈自由文法A→γ
タイプ-3正規文法A→aおよびA→aBまたはA→Ba

おおまかに言って、タイプ-1は左辺が複数ノードの場合もある句構造文法、タイプ-2は左辺が単一ノードの句構造文法、タイプ-3は二股枝分かれの句構造文法に相当する。このうち、正規表現はタイプ-3の正規文法に由来する。

(2012.8.5追記: タイプ-3は単なる二股枝分かれではなく、どちらか一方の枝が終端記号であるような二股枝分かれ)

実際に、簡単な正規表現を例にとって、それが正規文法でどのように表現されるか考えてみることにしよう。/ABC/という正規表現は、次のような句構造文法に対応する。

S1→A+S2
S2→B+S3
S3→C

正規表現でよく使われる繰り返し記号/*/、/+/、/?/などは次のような句構造文法で表現することができる。

/A*B/
S1→A+S1
S1→B

/A+B/
S1→A+S2
S2→A+S2
S2→B

/A?B/
S1→A+S2
S1→B
S2→B

実際には、最近の処理系で用いられる正規表現は、正規文法が本来表現できる範囲を超える文字列を表現できるとされる。が、大雑把な仕組みとしてはこんな感じである。