BT

Ready for InfoQ 3.0? Try the new design and let us know what you think!

メタプログラミングを使ってRubyにプロパティを追加する

| 作者 Werner Schuster フォローする 9 人のフォロワー , 翻訳者 近藤 修平 - (株)永和システムマネジメント フォローする 0 人のフォロワー 投稿日 2008年7月6日. 推定読書時間: 11 分 |

プロパティ、それは次の約束された地だ。少なくとも、この話題で持ちきりなJavaのブログ界隈から目を反らすことができないのであれば、だが。実はプロパティこそが、この世界を救う次世代の技術であり、喉から手が出る程求めていた銀の弾丸を与えてくれるもので、要するに、Java開発者達がJavaを使っていてよかったと実感させるような技術なのだろうか?ふん、プロパティが持つ絶大な力を理論付けるだけでは退屈だ。プロパティが役に立つのかどうか、Rubyに実際に機能を追加してみて、どういう具合になるのかを検証してみるのはどうだろうか?この記事にあるコードを書いたとしても、語彙や文法といった言語空間に悪影響は無いので、心配する必要はないのだから。

組み込みDSL

どうすればRubyにプロパティを追加できるだろうか?そこで、組み込みDSLの実装に挑戦してみよう。DSLをはじめる一番よい方法は、まず思ったままに書いてみて、それっぽく見えるものを大雑把に掴むことだ。

C#のプロパティをRubyで書いたらこんな感じになるだろうか。

class CruiseShip
property direction
property speed
end

最初の形では、Rubyの正しいコードではないものの、そう遠いわけでもない。このクラスをロードしようとすると、Rubyは"direction"と"speed"が未定義である、というエラーを出す。

"property"の呼び出しでプロパティ名をつけているのを、ロード時に評価されないようにしよう。そう。シンボルを使えばいいんだ!

class CruiseShip
property :direction
property :speed
end

このコードを構文解析してロードしてみた。だが、まだこんなエラーが残っている。NoMethodError: undefined method 'property' for CruiseShip:Class

これを解決するには、簡単な事実に気づけばよい。Rubyのクラスを定義した場合、それは単に宣言しているというだけではなく、クラスがロードされる時に実際に実行しているということなのだ。以下の行がロードされた時に、

 property :direction 

"property"という関数を探して、":direction"というパラメータとともに呼び出している。この時、"property"メソッドを使えるようにするには、どうすればよいだろうか?そこで、こうしてみよう。

def property(sym)
# do some stuff
end

class CruiseShip
property :direction
property :speed
end

やっと問題なくコードがロードされた。"property"関数の定義をファイルの頭に持ってくるのはあまり良いやり方とは言えないが、それについては後で取り上げることにする。そこで再利用できるような形に修正するつもりだ。

では"property"関数の実装を肉付けしていこう。"property"関数はクラス定義が実行されるときに呼び出されると言ったが、それは取りも直さず、その中でクラスにメソッドを追加することが可能であるということを意味している。これを利用して、クラスの一部として組み込みたいメソッドを定義することができる。"property"関数に以下のコードを加えてみよう。

define_method(sym) do
instance_variable_get("@#{sym}")
end

これは以下のコードをクラスに追加するのと同じ結果をもたらす。

def direction
@direction
end

つまり"direction"プロパティのゲッタとなる。同様にセッタを追加するにはこのようにすればよい。

define_method("#{sym}=") do |value|
instance_variable_set("@#{sym}", value)
end

ただ、これではまだまだ便利なものとは言えない...実は、同じことは、元々Rubyにある機能でattr_accessor :propertyを使えばできてしまうからだ。

なんだって?この記事で取り扱っている内容とこれまで実装してきた機能は、もともとRubyにあったというのか?いや、全てというわけではない。この記事では単にクラスにセッタとゲッタを追加する以上のものをプロパティに求めている。プロパティには、その値が変更された時に通知してくれるようなリスナを登録しておくことができるようになった方がよい。今になって考えてみると、古き良きオブザーバパターンはとても便利なものだった。ただ、それをJavaでやろうとすると、相変わらず膨大な量の退屈な入力作業とおきまりのコードを避けて通ることができない。オブザーバパターンには全てのリスナを呼び出すコードがある。そのコードはいつだって同じだ。だが、リスナに通知を始めるためにはリスナの登録が必要だ。そこにはリスナの登録/抹消をするような追加/削除メソッドがある筈だ。このコードはプロパティ名毎に用意しなければらないもので(add_direction_lisnerが必要だ...)、Javaでは自動的にそういったことを行う手段はない。

だがちょっと待って欲しい。これはRubyだ。メタプログラミングが力になってくれる。メタプログラミングは、コンピュータという怠惰な怠け者を使って、我々の代わりに退屈な仕事をさせるための技術で、これを使えば我々はバラの香りを楽しんだり、猫に餌を与えるための時間を増すことができるという寸法だ。

これはセッタを実装している部分で、すでに存在しているコードだ。

define_method("#{sym}=") do |value|
instance_variable_set("@#{sym}", value)
end

さて、こうしてみても、まだ通知することはできないだろうか?

define_method("#{sym}") do |value|
instance_variable_set("@#{sym}", value)
fire_event_for(sym)
end

ここにはまだリスナを取り扱うメソッドがない。"property"関数でやったように再びdefine_methodのトリックを使ってみよう。以下のように、特定のイベントの為のメソッドを定義する。

define_method("add_#{sym}_listener") do |x| 
@listener[sym] << x
end

リスナを削除したり、リスナにアクセスするメソッドは同じようにすればよい。リスナの配列を用意したりだとか、その他の残りのコードは読者の課題としておこう。(文句を言わないように。ほんの数行のコードばかりなのだから。)

コードを使ってみる

このコードがどう動くのか検証しよう。

h = CruiseShip.new
h.add_direction_listener(Listener.new)
h.add_bar_listener lambda {|x| puts "Oy... someone changed the property to #{x}"}
h.bar = 10

これは「おや、誰かがプロパティの値を 10 に変えたようです。」と出力されるだろう。

素晴らしい...そして簡単だ。しかしここでお楽しみの続きをする前に、ちょっとコードを整理してみよう。

よそ行きに着飾る

さて、どうやったら、このクラスの中にある機能が他でも使えるようになるだろうか?ここでミックスインというとても便利な機能の出番だ。ここで書くのは、クラスの定義にミックスインして使う、実にありきたりなRubyモジュールだ。不思議な感じがするかい?でも、こんなに簡単なんだ。

class Ship
extend Properties
end

こうすれば、Shipクラスの中でPropertiesモジュールの全ての機能が使えるようになる。これは、プロパティ呼び出しを実現する方法としては、なんと簡単な表記だろうか。他の言語では継承、つまり、クラスにメソッドを定義して、ユーザにそのクラスの継承を強制するという手段に訴える必要があったりする。一方ミックスインでは、クラスの継承階層には手を付けず、必要な機能をただ混ぜ合わせる(ミックスイン)するだけで済む(そう!それこそが名前の由来だ!)。

ここ言いたいのは、デモで作ったコードはミックスインを使って括り出せるということだ。

module Properties
def property(sym)
# all the nice code
end
end

こうすれば、以下のようにできる。

class Ship
extend Properties
property :direction
property :speed
end

また、こうすることで、このクラスはPropertiesミックスインを使っているという事実を明示することもできる。これもまたよい効果だ。コードを書いているとき、この拡張についてまだ精通していないならば、Propertiesミックスインのドキュメントを見るか、ソースを見ればよいのだと。

何かおもしろいことをやってみる

プロパティの基本的で便利な機能を使って、何かおもしろいことをしてみよう。「契約による設計(DbC)」のコンセプトでは、クラスに関するいくつかの制約と不変表明を定義するようになっている。静的言語では最初の取っかかりで次のように書くが、これはint型の値だけが受け入れ可能であることを意味している。

void foo(int x) 

もちろん、厳密にint型が何を指しているのかという問題はある。2の31乗という範囲にどんな意味があるというのか?":speed"というプロパティがあって、そこには0..300の値(あなたの持っている平均的な巡航船とは訳が違う)を入れたいとしたら、なおさらだ。他の切り口でこのことについて考えているのが、Gilad Bracha氏の「切替可能な型システム(Pluggable Type System)」という考え方(PDF・英語)で、つまりこれは、不十分な既存の型システムの代わりに、型とその範囲をより宣言的な方法で簡単に自分自身で定義することで、たくさんのif/elseの構文と防御的なプログラミングを使ってコードが雑然としてしまうのを防ぐというものだ。

さて、なぜ「契約による設計」と「切替可能な型システム」の講義をしているのだろうか?そう、我々は既に存在している言語を使っているからだ。プロパティの値についての制約を明確にするような便利機能をそこに追加しようとしている。

ここはひとつ、クリエイティブになって問題をどうやって解決したいのかを決めよう。範囲を使うのも、なんらかの指定した型を使うこともできる。ブロックを放り込んで使うことも出来る。

property(:speed) {|v| (v >= 0) && (v < 300)  } 

実装はこんなに簡単だ。

def property(x, &predicate)
define_method("#{sym}=") do |arg|
if(predicate)
if !predicate.call(arg)
return
end
end
instance_variable_set("@#{sym}", arg)
fire_event_for(sym)
end
end

このコードは、値の範囲を決めたり、型やnilのチェックをするような雑然としたコードは排除した上で、プロパティの値はクラスの内部に定義された範囲の中に常に収まる、という良い副作用はそのまま残している。見通しのよい一つの場所で、プロパティの全ての明確な制約を定義し、かつ、そこで完結している。

実際のところ、"speed"プロパティの制約については、もっと簡潔な方法で定義することができる。この記事の対象からは外れるが、課題として、以下のような感じで書けるように実装してはどうだろうか。

property :speed, in(0..300) 

ただ、これは正しくないRubyコードだ("in"というのはRubyキーワードだから)。まずコードを見える形にして、それをちゃんとしたRubyのコードに作り替えていく、というやり方から始めるのは効果的だと思う。

楽しんで欲しい。

最後に

確かに、後半ではプロパティの機能に何か追加するような話ではなかったが、プロパティとその通知の仕組みは、こういった型付けの話には関係していない方が良いだろうと単純に思ったからだ。とは言えRubyを使っているのだから、鋳型に流しこむように、あなたが望むものに対して言語を合わせてゆくことができる。この記事でやってきたのは、Rubyでできることの一例にしか過ぎない。

組み込みDSLに対する反論は多岐にわたっている。組み込みDSLがコードを読みにくくしているという意見もある。それは事実でもある。ちょうど、"print(x)"というコードが読みにくいのと同じように。この関数の呼び出しが何をしているのかは、一体どうやったら知ることができるだろうか?結局、ドキュメントを読むか、ソースコードを眺めるぐらいしかできない。でもそれは、DSLを実装するのと何か違うのだろうか?違いは無い。

ここで使った技法はとても単純なもので、「ポリモーフィズム」や「再帰的な型の実現」(や今や市民権のあるアメリカ生まれのいろんな用語ならなんでも)といったキーワードに怯えることのない開発者であれば、全員がすぐ理解できるようなもので、これらの技法が使えるようになると、ずっと簡潔で適切なメンテナンス可能なコードが書けるようになる。

原文はこちらです:http://www.infoq.com/articles/properties-metaprogramming
(このArticleは2007年4月18日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

こんにちは

コメントするには InfoQアカウントの登録 または が必要です。InfoQ に登録するとさまざまなことができます。

アカウント登録をしてInfoQをお楽しみください。

あなたの意見をお聞かせください。

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする
コミュニティコメント

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする

HTML: a,b,br,blockquote,i,li,pre,u,ul,p

このスレッドのメッセージについてEmailでリプライする

ディスカッション
BT