K.Sasada's Home Page

こめんとのついか

こめんとこめんと!

message

please add long comment :).

_20(Thu)

Ruby VM アドベントカレンダーの 20 日目です.

昨日は Module#prepend を紹介しました.include ではメソッド探索の順序で後ろに追加していたものを,前に追加するものでした.

復習のために,昨日のプログラムを再掲してみます.

class C0
  def m; p :C0; end
end

module M
  def m; p :M; super; end
end

class C < C0
  prepend M                # include だったのを prepend にしている
  def m; p :C; super; end
end

obj = C.new
obj.m

#=>
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
:M
:C
:C0

C のオブジェクトにメソッド m を実行したとき,M#m が先に呼ばれていることがわかります.

昨日は「Module#prepend で,これまで出来なかったようなことができるようになります.試してみて下さい.」と結んだのですが,そもそもこれ,どんな時に使うんでしょうか.

一番ありそうな使い方としては,あるメソッドの開始と終了時に特定の処理をさせたい場合です.例えば,Array#each が呼ばれたとき,ログを出力するような例を記述してみます.

module EachLogger
  def each(*args, &block)
    puts "Enter `each'"
    super
    puts "Leave `each'"
  end
end

class Array
  prepend EachLogger
end


p :each
[1, 2, 3].each{}
p :find
[1, 2, 3].find(1)
p :find_all
[1, 2, 3].find_all{|e| (e % 2) == 1}

#=>
ruby 2.0.0dev (2012-12-21 trunk 38515) [i386-mswin32_100]
:each
Enter `each'
Leave `each'
:find
:find_all
Enter `each'
Leave `each'

はい,このように each メソッドが実行されたとき,EachLogger によってログが出力されることがわかりました.

ちなみに,find メソッドでは each は使われていないことがわかります.逆に find_all メソッドでは使われていますが,これは Enumerable#find_all メソッドが each メソッドを呼び出しているからです.

このように,メソッドの開始と終了時に何かしらの処理を行うということを around と言ったりします.もちろん,開始時だけ,終了時だけ,ということも出来ます.


さて,先ほどは EachLogger という each メソッドをロギングするモジュールを作りましたが,どうせならあるクラスのすべてのメソッドのログを出力するものを作ってみましょう.

def prepend_method_logger klass
  mod = Module.new
  klass.instance_methods(false).each{|m|
    mod.module_eval %Q{
      def #{m}(*args)
        puts "Enter `#{m}'"
        super
        puts "Leave `#{m}'"
      end
    }
  }
  klass.module_eval{
    prepend mod
  }
end

class C
  def m; puts "C#m"; end
  def n; puts "C#n"; end
end

prepend_analyse_module C

obj = C.new
obj.m
obj.n

#=>
Enter `m'
C#m
Leave `m'
Enter `n'
C#n
Leave `n'

prepend_method_logger は指定されたクラスに定義されているメソッドすべてに対して EachLogger#each のように,ログを出力するメソッドを定義しています.module_eval を利用しているのが黒魔術的ですね.

実行結果で,C#m,C#n の実行時に,きちんとログが出力されていることがわかると思います.


さて,これを利用して,もうちょっと便利かもしれないツールを作ってみましょう.

あるクラスのメソッドが,どのような引数を受け付け,そして返値に何を返しているのか統計をとってみます.これを作ることで,意図しない引数を受け付けているかどうかがわかります.

def prepend_analyse_module klass
  $analyse_result = {} unless defined?($analyse_result)
  stat = $analyse_result[klass] = {}

  mod = Module.new
  klass.instance_methods(false).each{|m|
    stat[m] = [Hash.new{|h, k| h[k] = Hash.new(0)}, # args
               Hash.new(0)]                         # return
    src = %Q{
      def #{m}(*args)
        args.each.with_index{|e, i|
          $analyse_result[#{klass.name}][:#{m}][0][i][e.class] += 1
        }
        result = super
        $analyse_result[#{klass.name}][:#{m}][1][result.class] += 1
      end
    }
    mod.module_eval src
  }
  klass.module_eval{
    prepend mod
  }
end

class C
  def m foo, bar; puts "C#m"; end
  def n baz; puts "C#n"; end
end

prepend_analyse_module C

obj = C.new
obj.m 'a', 1
obj.m :sym, 1.2
obj.n /x/

require 'pp'
pp $analyse_result

#=>
C#m
C#m
C#n
{C=>
  {:m=>[{0=>{String=>1, Symbol=>1}, 1=>{Fixnum=>1, Float=>1}}, {NilClass=>2}],
   :n=>[{0=>{Regexp=>1}}, {NilClass=>1}]}}

色々面倒くさかったので,$analyse_result というグローバル変数に情報を全部詰め込むようにしてみました.

結果を見ると,C#m は 0 番目の引数に String および Symbol を 1 個ずつ,1 番目の引数に,... という情報がわかります.

これを有効にして,テストケースを走らせてみると,意図しない型が来ているかどうか,チェックできるかもしれません.

では,今日はこの辺で.


好きなだけ長いコメントをどうぞ。

お名前


back

tton 記述が使えます。YukiWikiな記述してりゃ問題ありません。

「行頭に#code」 と、「行頭に#end」 で挟むと、その間の行は pre で囲まれます。プログラムのソースを書くときに使ってください。

例:

#code

(なんかプログラム書く)

#end

リンクは

[[なまえ|http://www.example.org]]

とか

[[http://www.example.org]]

で貼れます。

$Date: 2003/04/28 10:27:51 $