Rubyメタプログラミング技術 (Ruby Metaprogramming techniques)
最近、メタプログラミングについて考えることが多く、このテクニックの例や解説をもっと見たいと思います。良くも悪くも、メタプログラミングは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やメタプログラミングの専門家であると主張するつもりはありません。
関連
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン