1. ホーム
  2. スクリプト・コラム
  3. ルア

Rubyメタプログラミング技術 (Ruby Metaprogramming techniques)

2022-02-13 21:16:44

最近、メタプログラミングについて考えることが多く、このテクニックの例や解説をもっと見たいと思います。良くも悪くも、メタプログラミングはRubyのコミュニティに浸透し、様々なタスクを達成し、コードを簡素化するための標準的な方法となっています。そのようなリソースが見つからないので、一般的なRubyのテクニックに関する記事をいくつか紹介します。これらは、他の言語からRubyに移ってきたプログラマや、Rubyメタプログラミングの楽しさをまだ体験していないプログラマにとって有用かもしれません。

1. singleton-classの使用 singleton-classを使用します。

  一つのオブジェクトを操作する方法の多くは、そのシングルトン・クラスを操作することに基づいており、これによってメタプログラミングが容易になります。シングルトン・クラスを取得する古典的な方法は、次のようなコードを実行することです。

コピーコード コードは以下の通りです。

sclass = (class << self; self; end) 

  RCR231では、Kernel#singleton_classメソッドをこのように定義することを提案しています。
コピーコード コードは以下の通りです。

module Kernel  
  def singleton_class  
    class << self; self; end 
  end 
end 

  以下、この方法を使ってみます。

2. サブクラスを書き換えるクラスメソッドを使ったDSL サブクラスを書き換えるクラスメソッドを使ったDSLを書く

  クラス情報を定義するDSLを作りたいとき、最もよくある問題は、フレームワークの他の部分が使えるように、どのように情報を表現するかということです。例として、ActiveRecordのモデルオブジェクトを定義してみましょう。

コピーコード コードは以下の通りです。

class Product < ActiveRecord::Base  
  set_table_name 'produce'   
end 

  この例で興味深いのは、set_table_nameの使い方です。これはどのように機能するのでしょうか?さて、ここにはちょっとしたマジックがあります。ここでは、それを実装する1つの方法を紹介します。

コピーコード コードは以下の通りです。

module ActiveRecord  
  class Base  
    def self.set_table_name name  
      define_attr_method :table_name, name  
    end 
    def self.define_attr_method(name, value)  
      singleton_class.send :alias_method, "original_#{name}", name  
      singleton_class.class_eval do   
        define_method(name) do     
          value  
        end 
      end 
    end 
  end   
end 

  この場合、Productクラスのシングルトン・クラスを取得する必要があるが、ActiveRecord::Baseを変更したくない。シングルトン・クラスを使用することによって、これを実現する。元のメソッドのエイリアスを作成し、値を返す新しいアクセッサを定義します。ActiveRecordがテーブル名を必要とする場合は、直接アクセッサを呼び出すことができます。このようにメソッドとアクセサを動的に作成する手法は、特にRailsのシングルトンクラスでは非常によく見られます。

3. クラスとモジュールを動的に作成する

  Rubyでは、クラスやモジュールを動的に作成したり、変更したりすることができます。Structクラスはその最たる例でしょう。

コピーコード コードは以下の通りです。

PersonVO = Struct.new(:name, :phone, :email)  
p1 = PersonVO.new(:name => "Ola Bini") 

  これで新しいクラスが作成され、PersonVOに割り当てられ、そのクラスのインスタンスが作成されます。草案から新しいクラスを作成し、新しいメソッドを定義することも簡単です。
コピーコード コードは以下の通りです。

c = Class.new 
c.class_eval do 
  define_method :foo do 
    puts "Hello World" 
  end 
end 
c.new.foo # => "Hello World" 

  Struct以外にも、SOAP4RやCampingで簡単にクラスを作成する例があります。Campingは、コントローラやビューに継承されるクラスを作成するための特別なメソッドを持っているので、特に興味深いです。Campingの興味深い機能の多くは、この方法で実装されています:。
コピーコード コードは以下の通りです。

def R(*urls); Class.new(R) { meta_def(:urls) { urls } }   
end 

  This allows the controller to be created like this.
class View < R '/view/(\d+)' 
  def get post_id  
  end 
end 


  また、この方法でモジュールを作成し、そのモジュールをクラスに含めることもできます。

4. method_missing を使って面白いことをする method_missing を使って面白いことをする

  クロージャ(ブロック)の他に、メソッドミッシングはおそらくRubyの最も強力な機能であり、最も悪用しやすい機能のひとつでもある。メソッドミスをうまく使えば、超シンプルなコードになったり、必要不可欠なコードになったりするものもある。良い例(Camping)は、Hashを拡張することです。

コピーコード コードは以下の通りです。

class Hash 
  def method_missing(m,*a)  
    if m.to_s =~ /=$/  
      self[$`] = a[0]  
    elsif a.empty?    
      self[m]  
    else 
      raise NoMethodError, "#{m}" 
    end 
  end 
end 

  そして、このハッシュは次のように使うことができます。
コピーコード コードは以下の通りです。

x = {'abc' => 123}  
x.abc # => 123  
x.foo = :baz 
x # => {'abc' => 123, 'foo' => :baz} 

  このように、ハッシュが存在しないメソッドを誰かが呼び出した場合、内部のコレクションが検索されます。メソッド名が = で終わっている場合は、同じ名前のキーが割り当てられます。

  もう一つの良いmethod_missingのトリックは、Markabyで見つけることができます。次の引用コードは、CSS クラスを含むあらゆる XHTML タグを生成します。

コピーコード コードは以下の通りです。

body do 
  h1.header 'Blog' 
  div.content do 
    'Hellu' 
  end 
end 

が生成されます。
コピーコード コードは以下の通りです。

<body> 
  <h1 class="header">Blog</h1> 
  <div class="content"> 
    Hellu  
  </div> 
</body> 

  このような関数の大半、特にCSSのクラス名では、method_missingによってself属性を設定し、selfを返すようになっています。

5. メソッドパターンでのディスパッチ メソッドパターンでのディスパッチ

  これにより、予測不可能なメソッドに対するスケーラビリティを容易に実現することができる。私は最近、小さな検証フレームワークを作りました。コアとなる検証クラスは、check_ で始まる独自のメソッドをすべて見つけて呼び出し、新しい検証を簡単に追加できるようにします。
メソッド.grep /^check_/ do |m|.  
  self.send m  
終了 

  これは非常にシンプルで、信じられないほど強力です。Test::Unitではこの方法を随所で使っています。

6. メソッドの置き換え メソッドの置き換え

  あるメソッドが思い通りに実装されていなかったり、半分しかできていなかったりすることがあります。オブジェクト指向の標準的な方法は、継承してオーバーロードし、親メソッドを呼び出すことです。これは、オブジェクトのインスタンス化をコントロールできる場合にのみ有効ですが、そうでない場合も多く、継承は無意味です。同じ機能を得るには、古いメソッドの名前を変え(エイリアス)、新しいメソッド定義を追加して古いメソッドを呼び出し、古いメソッドの前後の条件が保存されるようにすればよいのです。

コピーコード コードは以下の通りです。

class String 
  alias_method :original_reverse, :reverse 
  def reverse   
    puts "reversing, please wait... " original_reverse  
  end 
end 

  極端な使い方としては、一時的にメソッドを変更して、それを元に戻すということがあります。例として
コピーコード コードは以下の通りです。

def trace(*mths)  
  add_tracing(*mths) # aliases the methods named, adding tracing      
  yield 
  remove_tracing(*mths) # removes the tracing aliases  
end 

  この例は、add_tracing と remove_tracing の典型的な書き方を示しています。これは記事 1 のシングルトン・クラスに依存しています。
コピーコード コードは以下の通りです。

class Object    
  def add_tracing(*mths)      
    mths.each do |m|   
      singleton_class.send :alias_method, "traced_#{m}", m   
      singleton_class.send :define_method, m do |*args|  
        $stderr.puts "before #{m}(#{args.inspect})" 
        ret = self.send("traced_#{m}", *args)  
        $stderr.puts "after #{m} - #{ret.inspect}" 
        ret  
      end 
    end    
  end 
  def remove_tracing(*mths)     
    mths.each do |m|  
      singleton_class.send :alias_method, m, "traced_#{m}" 
    end 
  end 
end 
"abc".add_tracing :reverse 

  これらのメソッドをモジュールに追加すれば(ちょっと違うので書けるかどうか見てみてください!)、インスタンスではなくクラスに対してトレースを追加したり削除したりすることも可能です。

7. NilClass を使用して、Introduce Null Object リファクタリングを実装する。

  Fowlerのリファクタリングにおいて、"Introduce Null Object"とは、オブジェクトが存在するか、空の時にあらかじめ定義された値を持つことである。典型的な例としては、以下のようなものがある。

コピーコード コードは以下の通りです。

name = x.nil? ? "default name" : x.name 

  現在のJavaベースのリファクタリングでは、nullと同様のサブクラスを作成することが推奨されています。例えば、NullPerson は Person を継承し、name メソッドをオーバーライドすると、常に "デフォルト名" を返すようにします。しかし、Rubyでは、次のようにクラスを開いて、そうすることができます。
コピーコード コードは以下の通りです。

def nil.name; "default name"; end 
x # => nil  
name = x.name # => "default name" 

8. evalの様々なバージョンを学ぶ evalの様々なバージョンを学ぶ

  Rubyには、いくつかのバージョンの評価方法があります。その違いと利用シーンを理解することが重要です。eval、instance_eval、module_eval、class_evalがあります。まず、class_evalはmodule_evalのエイリアスです。次に、evalは他とは少し違います。evalは文字列しか実行できませんが、他のものはブロックを実行できます。つまり、evalは何をするにも最後の選択で、その用途はありますが、大半の場合はinstance_evalやmodule_evalを使ってブロックを実行すべきです。

  eval は、環境がバインディングを提供していない限り、現在の環境の文字列を実行します。(#11 参照)

  instance_eval は受信者 (reveiver) のコンテキストで文字列やブロックを実行します。指定がない場合は self が受信者として振る舞います。

  module_evalは呼び出し元のモジュールのコンテキストで文字列やブロックを実行します。これはモジュールやシングルトンクラスで新しいメソッドを定義する場合により適しています。もし、String.instance_evalでfooメソッドを定義するとString.fooが得られ、module_evalを使うとString.new.fooが得られます。

  module_eval はほとんど常に適用されます。eval の使用はできるだけ避けましょう。


9. インスタンス変数のイントロスペクト

  Railsがコントローラのインスタンス変数をビューで動作させるために使うトリックのひとつに、オブジェクトのインスタンス変数をイントロスペクトするものがあります。これはカプセル化を大きく崩す可能性がありますが、非常にうまくいくこともあります。これは instance_variables, instance_variable_get, instance_variable_set で簡単に行えます。すべてのインスタンス変数を一方から他方へコピーするには、次のようにします。

コピーコード コードは以下の通りです。

from.instance_variables.each do |v|  
  to.instance_variable_set v, from.instance_variable_get(v)  
end 

10. ブロックからProcsを作成し、公開する ブロックからProcsを作成し、送信する

  Procを変数にインスタンス化して公開することで、多くのAPIを使いやすくすることができます。これは、MarkabyがCSSのクラス定義を管理するために使っている手法の一つである。ブロックをProcに変換するのは簡単です。
def create_proc(&p); p; end 
create_proc do 
  puts "hello" 
end # => #<Proc ... > 

  呼び出しも簡単です。
p.call(*args) 

  lambdaで作るべきメソッドをprocで定義したい場合は、returnとbreak:を使います。
p = lambda { puts "hoho"; return 1 }.  
define_method(:a、&p) 

  ブロックがある場合、method_missing はそのブロックを呼び出します。
def method_missing(name, *args, &block)  
  block.call(*args) if block_given?  
終了 
thismethoddoesntexist("abc","cde") do |*args|.  
  p args  
end # => ["abc","cde"]. 


11. バインディングを使用して評価を制御する

  もしevalを使う必要があるなら、どの変数が有効かを制御することができます。このとき、カーネルメソッドのバインディングを使って、バインドされているオブジェクトを取得することができます。例えば

コピーコード コードは以下の通りです。

def get_b; binding; end 
foo = 13  
eval("puts foo",get_b) # => NameError: undefined local variable or method `foo' for main:Object 

  ERbとRailsは、どのインスタンス変数が有効かを設定するためにこのテクニックを使用します。例えば
コピーコード コードは以下の通りです。

class Holder  
  def get_b; binding; end 
end 
h = Holder.new 
h.instance_variable_set "@foo", 25  
eval("@foo",h.get_b) 

  これらのヒントとテクニックによって、あなたにとってメタプログラミングが明確になったことを願っています。私はRubyやメタプログラミングの専門家であると主張するつもりはありません。