2013年8月22日木曜日

Rubyのメタプログラミング技術


動的言語としてのRubyの特徴を抑えておく。

◆ 動的ディスパッチ
実行時にメソッドを定義する。

class MyClass
  def my_method(arg)
    arg * 2
  end
end

MyClass.new.my_method(10) # => 20
MyClass.new.send(:my_method, 10) # => 20

普通にメソッドを利用する場合も、sendを使ってメソッドを
呼ぶのも同じ結果を得るのだが、後者に何かメリットはあるのだろうか。
send()を使えば、呼び出すメソッドはただの引数となり、
コードの実行時にメソッドを動的に選択することができるのである。
※':文字列'はただのシンボル。


send()はパターンディスパッチにも使われる。
パターンディスパッチによって
メソッド名のパターンに基づいてメソッドを振り分けることが可能である。

class MyClass
  def my_first_method()
    puts "first"
  end

  def my_second_method()
    puts "second"
  end
end

obj = MyClass.new
obj.methods.each do |m|
  obj.send(m) if m.to_s =~ /^my_/
end



◆ 動的メソッド
その場でメソッドを定義する。

class MyClass
  define_method :my_method do |arg|
    arg * 2
  end
end

MyClass.new.my_method(10) # => 20


他の例も示そう。

同じようなインスタンスメソッドを書かなくてはならないことがある。
class MyClass
  def success
    @state = :success
  end

  def error
    @state = :error
  end
end

動的メソッドを使えば重複を取り除ける。
class MyClass
  [:success, :error].each do | method |
    define_method method do
      @state = method
    end
  end
end

クラスマクロとしてクラスの定義時に類似メソッドをまとめて作ってもいいだろう。
class MyClass
  def self.states(*args)
    args.each do |arg|
      define_method arg do
        @state = arg
      end
    end
  end

  states :success, :error
end


◆ ゴーストメソッド
定義していないメソッドに応答する。

class MyClass
  def method_missing(name, *args)
    args[0] * 2
  end
end

MyClass.new.my_method(10) # => 20


もう少し実用的な例として、
ゴーストメソッドを使ってセッターとゲッターを定義する。

class MyClass
  def initialize
    @attributes = {}
  end

  def method_missing(name, *args)
    attribute = name.to_s
    if attribute =~ /=$/
      @attributes[attribute.chop] = args[0]
    else
      @attributes[attribute]
    end
  end
end

MyClass.new.name = "shinya"



◆ 遅延評価
Procやlambdaにコードとコンテキストを保管して後で評価する。

obj = Proc.new {|x, y| x * y} # もしくは
obj = lambda {|x, y| x * y}
obj.call(10, 20)

class MyClass
  def store(&block)
    @my_code = block
  end

  def execute(arg1, arg2)
    @my_code.call(arg1, arg2)
  end
end

obj = MyClass.new
obj.store {|x, y| x * y}
obj.execute(10, 20) # => 200



◆ selfについて
突然だが、Rubyの基礎としてselfをおさえておく。
selfの理解が曖昧だとRubyの黒魔術を理解しきれない点があるためである。

メソッドが呼び出された時のオブジェクトをレシーバという。
メソッドを呼び出すとそのレシーバがselfになる。
selfのことをカレントオブジェクトとも呼ぶ。

さて、メソッド内でさらにメソッドを呼び出すコードを書いてみる。
どちらのprintメソッドが呼ばれるか分かるだろうか。

継承チェーンとしてはincludeの順に従いこうなる。
モジュールをインクルードするとそのクラスの真上の継承チェーンに
モジュールが挿入される。

      Projector
         ↑
    Copier
         ↑
book → Book


module Projector
  def print
    puts "projector"
  end
end

module Copier
  def print
    puts "copier"
  end

  def takeout
    print
  end
end

class Book
  include Projector
  include Copier
end

Book.new.takeout # >= copier


直感と反した、と感じる人も多いだろう。
メソッドを明示せずに呼び出すメソッドはすべてselfに対しての呼び出しとなり、
再度継承チェーンをたどっていくのである。


一方、クラスやモジュールの定義の中では、selfは定義されたクラスやモジュールになる。

class MyClass
  self # => MyClass
end

カレントオブジェクトのことをselfと呼ぶのではないか、
と疑問を持ったかもしれない。
思い出してほしい。
クラスはClassクラスのインスタンスである。
クラスもオブジェクトにすぎない。



◆ スコープ1
スコープはclass, module, defといったスコープゲートで区切られている。
大事なところなのでもう一度。
スコープはclass, module, defといったスコープゲートで区切られている。

トップレベルドメインで定義された変数を
クラスの中で利用するにはどうすればいいか。

my_var = "test"

・classのスケープゴートの飛び越え
classをスコープゲートではない何かに置き換えればよい。

MyClass = Class.new() do
  my_var #参照可能になる

  def my_method
    my_var # ただし、この中では参照できない
  end
end

Class.new()がclassの置き換えになり、
classのスコープゲートを飛び越えることができる。

Class.new()とは何であろうか。
クラスはClassクラスのインスタンスにすぎない。
Classクラスからクラスオブジェクトを作ると考えればよい。

Class.new()は引数にスーパークラスを指定できる。

MyClass = Class.new(Array) do
end

以下と同じである。

class MyClass < Array
end


・defのスケープゴートの飛び越え
動的メソッドで説明したdefine_methodを使う。

MyClass = Class.new() do
  my_var #参照可能

  define_method :my_method do
    my_var #参照可能
  end
end

また、sendを使ってもいいだろう。
MyClass = Class.new() do
  my_var #参照可能

  send :define_method, :my_method do
    puts my_var #参照可能
  end
end

MyClass.new.my_method() でmy_varが見えるだろう。

ちなみに、クラス外でdefin_methodを使う場合は
以下のようにしないといけない。
Kernel.send :define_method, :my_method do
  puts my_var
end

トップレベルではObjectをクラスとするmainオブジェクトの中にいるため、
プライベートメソッドであるdefine_methodは呼び出せないのである。
sendはプライベートメソッドも呼び出せる(Object.sendでもいける)。

self # => main
self.class # => Object


・moduleのスケープゴートの飛び越え
moduleに関しても、Class.new()と同様にModule.new()を利用すればよい。



◆ スコープ2
スコープを破るよく使う別の例についてもまとめておく。
・instance_eval()
オブジェクトのコンテキストでブロックを評価することができる。

class MyClass
  def initialize
    @my_var = 1
  end
end

my_var = 2
my_obj = MyClass.new
my_obj.instance_eval do
  @my_var # >= 1
  my_var # >= 2
end

フラットスコープになるため、ローカル変数にも
アクセスできるようになる。


instance_exec()を使うと引数を渡すことができる。
my_obj.instance_exec(10) do |arg|
  @my_var + arg # => 11
end


・class_eval()
既存のクラスのコンテキストでブロックを評価することができる。

class MyClass; end

MyClass.class_eval do
  def my_method; end
end

MyClass.new.my_method

instance_eval()はselfに変更を加えるだけだが、
class_eval()はカレントクラスにも変更を加えられる。
class_eval()もフラットスコープを持つ。



◆ 特異メソッド
特定のオブジェクトだけにメソッドを追加することができる。

str = "test"

def str.check()
  self == "test" # => true
end

Stringクラスのほかのメソッドには影響はない。



◆ 特異メソッドとクラスメソッドの関係性
まずは当たり前のように使うことのあるクラスメソッドである。

class MyClass
  def MyClass.my_method; end
end

MyClass.my_method

リファクタリングのしやすさからselfを使うことが普通だろうか。

class MyClass
  def self.my_method; end
end

ここに特異メソッドとの関係性をうかがうことができる。
クラスはclassクラスのオブジェクトにすぎないので、
クラスメソッドはクラスというオブジェクトの
特異メソッドであるのである。



◆ 特異クラス
特異クラスとは特異メソッドを実装するために使われるクラスである。
特異メソッドについては説明した通り、
特定のオブジェクトだけに有効なメソッドのことである。
では、オブジェクトの特異メソッドはクラス内のどこで
定義されるのであろうか。

実は通常のクラスの裏に、特別なクラスが存在している。
これが特異クラスと呼ばれ、特異メソッドはここに住んでいる。

通常は見えない特異クラスはどのようにオープンすればいいか。
特別な構文が存在する。
<<を使うと特異クラスのスコープに入り込むことができる。

str = "test"

def str.check()
  self == "test" # => true
end

    ↓

class << str
  def check()
    self == "test" # => true
  end
end


クラスメソッドの例も示そう。

class MyClass
  def self.my_method; end
end

    ↓

class MyClass
  class << self
    def my_method; end
  end
end

MyClass.my_method



◆ モジュール取り込み
module MyModule
  def my_method; end
end

class MyClass
  include MyModule
end

MyClass.new.my_method

オブジェクトを作成後は、
インスタンスメソッドmy_methodが利用できるようになる。

では、MyClassのオブジェクトが利用するインスタンスメソッドではなく、
クラスメソッドとして利用するにはどうすればいいだろうか。
MyClass.my_method
として使いたい場合はどうすればいいか、ということである。

class MyClass
  class << self
    include MyModule
  end
end

クラスメソッドを特異クラスで作った場合と同じである。


オブジェクト拡張も同じ理屈である。

module MyModule
  def check()
    self == "test"
  end
end

str = "test"

class << str
  include MyModule
end

str.check() # => true


クラスメソッドとしてモジュールを利用する場合は、
普通はincludeではなく、extendを用いるだろうか。
ただし、やっていることは上記の通りである。

class MyClass
  extend MyModule
end

MyClass.my_method



◆ フックメソッド
イベントをキャッチできるメソッドがある。
継承時のinheritedや、モジュールのミックスイン時の
includedなどである。

class String
  def self.inherited(subclass)
    puts subclass
  end
end

class MyString < String; end # => MyString


Stringクラスのクラスメソッドとしてinheritedが
自動で呼び出される。
引数のsubclassはMyStringになる。


もう一つ例をあげてみる。

module MyModule
  def self.included(includer)
    includer.extend(MyMethods)
  end

  module MyMethods
    def my_method
      "this is class method"
    end
  end
end

class MyClass
  include MyModule
end

MyClass.my_method # => "this is class method"

MyClassのクラスメソッドとしてincludedが呼び出される。
引数のincluderはMyClassを指す。
そこから、extendしてクラスメソッドを定義する。


フックメソッドを簡易的に自作してみる。
クラスメソッドとしてincludeを用意し、
実際のincludeをオーバライドさせ、
そこからsuperで元のincludeを呼び出す。
super呼び出し時は引数もそのまま引き継がれる。

module MyModule1; end
module MyModule2; end

class MyClass
  def self.include(*modules)
    super
  end

  include MyModule1, MyModule2

end


◆ アラウンドエイリアス
メソッドにエイリアスをつけて再定義する。

String#length()メソッドを利用時に、
処理時間を埋め込むようにラップをする。

class String
  alias :old_length :length

  def length
    start = Time.now
    result = old_length
    time_taken = Time.now - start
    puts "length took #{time_taken} seconds"
    result
  end
end

"abc".length # => length took 6.0e-06 seconds
             # => 3


新しいメソッドを定義しても、
古いメソッドもエイリアスを使って呼び出せる点がポイントである。


以上までが理解できたら応用編。
クラスマクロの実装をしてみる。


(参考)
メタプログラミング Ruby 



0 件のコメント:

コメントを投稿