BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル オブジェクト・メタデータにシングルトンクラスを使う

オブジェクト・メタデータにシングルトンクラスを使う

ブックマーク

オペレーションあるいはAPI呼び出しの結果、多数のオブジェクト、つまりオブジェクトグラフができてしまった。やるべきことは、データを解析し、解析結果をグラフのメタデータとして保存することである。

あまりにも抽象的すぎる?コンパイラがどう機能するか思い浮かべてみよう。パーサは、コードをツリー構造で表す構文木(あるいは抽象構文ツリー〔Abstract Syntax Tree=AST〕)の形で出力する。次に、記号テーブルで記号を集めたり、型推論したり、その型を使って型検査を行ったり、など、多数のアルゴリズムがパスの形でツリー上に広がる。

でも待って。最後の2パスに問題が示されている。型推論コードは、型検査が使うデータをどこに保管するのか? 必要とされる場所にデータを保管すれば、最も好都合だろう。すなわち式ノード(AST内)がFoo型を返すと分かれば、その情報を正にノードに保管しておくのが最良だろう。

ASTで機能するツールに注目して、ソリューションを説明しよう。コンパイラではないが、同じような要件のツールである。RubyではParseTree(サイト・英語)ライブラリがASTとしてRubyソースコードを返す。以下は例である。

[:vcall, :obj, :say_hello, [:array, [:lit, 42], [:lvar, :foo]]] 

これは ParseTree ASTとして表したRuby コードである。今回は記事でVMではないので[GLOVA1]、グラフとして整理したオブジェクトやオブジェクト参照の取り扱いが少々難しい。そのため、ParseTreeのS-exprツリー表現を使う。 S-exprは入れ子になったリストで、各リストがツリー内のノードを表し、最初の要素がノード型を指定している。サンプルでは、ノードが仮想メソッド(vcall)への呼び出しを表す。パラメータはレシーバ(呼び出されたメソッド内のそれ「自身」)、メソッド名、パラメータである。

ツール向けのアイデアは型検査であり、動的解析ツールあるいは自動リファクタリングになるだろう。そうした種類のツールが機能するための必要条件の1つが型推論で、変数あるいは式の型を決定する。例えば、次のASTはto_sメソッドへの呼び出しを表す。

[:vcall, :obj, :to_s] 

これに関して集めることができる情報は何で、その情報で何ができるのか? 例えば戻り値の型だ。その判断は難しいが、to_sはオブジェクトの文字列表現を返すメソッドなので、「文字列」を返すとだけ言っておこう。これは必ずしも真実ではなく、推測である。コード補完機能のようなツールでは十分だろうが、こうした事柄が100%正確というわけにはいかない。しかし場合によっては、もっとたくさんの情報を集めることもでき、そうなればアナライザはいっそう正確な情報を判断できるだろう。

アナライザはツリーに取り掛かり、そして決定したメタデータをASTにアノテートする。コードベースをモジュールにしておくための良いアイデアは、アナライザをメタデータ消費者から分離しておくことである。メタデータ消費者とはつまり、ASTを動かして[GLOVA2]メタデータに何らかの処置を施すコードのことである。例えば、Rubyのエディタは、スーパークラスで別のメソッドをオーバーライドするメソッドを強調表示することができる。

ソリューション

かいつまんで話すと、ParseTreeノードをアノテートするソリューションは以下のとおりである。

node = [:vcall, :obj, :to_s] 

def node.set_metadata(key, value)
 @_metadata ||= Hash.new
 @_metadata[key] = value
end
def node.metadata
@_metadata ||= {}
end

node.set_metadata(:type, :String)

一体何をやっているのか?

秘密のソース:シングルトンクラス

Rubyではあらゆるオブジェクトがクラスのインスタンスである。他の多数のOOP言語とは異なり、Rubyではオブジェクトのクラスを変更できる。これをオープンクラスと混同してはいけない。Rubyでは、ランタイム時にさえ、クラスの修正が可能である。シングルトンクラスは類似しているが、単一オブジェクトのクラスのみに影響を与える点で異なる。小さな相違に見えるかもしれないが、影響を受けるオブジェクトだけにクラス修正の効力を制限するという点で、大きな利点がある。それに比べてオープンクラスは、こうしたクラスの全コードと全オブジェクトのクラス定義を変更する。有用だが、他人のコードにも手を出すことになり、そして共通クラスのあまりにも多数のコードに変更が起これば、既存メソッドとの名称衝突が起こる可能性がある。ParseTreeノード(普通のRuby配列)のケースでは、ヒープ上のすべての配列ではなく、実際に使用された配列オブジェクトだけが影響を受けることを意味する。

別の見方をすると:シングルトンクラスを使えば、クラスを作成して使用するコードに局所的にクラス変更を加えることができ、外に見せる必要はまったくない。他方、オープンクラスはグローバルな変更である。クラス名はグローバル変数であり、すなわち「文字列」は文字列を表すクラスオブジェクトを指す。より適用範囲を絞った変数(ローカル変数、メンバー変数)がグローバル変数より望ましいのと同様に、オープンクラスよりシングルトンクラスが望ましいのである。

もちろん、どちらのソリューション(シングルトンもしくはオープンクラス)を選ぶかは個々の状況に左右される。 オープンクラスでは、クラスへの変更は1度で、その後は、そのクラスの全オブジェクトにメソッドが加わっている。シングルトンクラスでは、クラスへの変更(すなわち、シングルトンクラスの作成とそれを指し示すオブジェクトのクラスポインターの変更)は、すべての変更で発生する。

構文は前述したように単純である。

def object_variable.method_name()
# code
end

オブジェクトを指し示す変数はメソッド名のプレフィックスになっている。これよりも柔軟で、モジュール的な方法はMixinの使用である。そうすればモジュールでアスペクトを処理するメソッドをまとめて一括でミックスできる。例:

module Metadata 
def set_metadata(key, val)
@_metadata[key] = val
end
def metadata
@_metadata ||= {}
end
end
x = [:vcall, :obj, :to_s]
x.extend Metadata
x.set_metadata(:type, :String)

Mixinでは、モジュール内で定義された機能をクラス内へとミックスできる。このサンプルでは、ノードオブジェクトのシングルトンクラスである。もう一度言うが、このオブジェクトに限ってこうしたメソッドが存在することになる。

別の例 - 不要コードの削除

静的アナライザコードが抽象的過ぎるなら、別のツールを試してみよう。不要コードのリムーバだ。「不要コード」とは、単に何もしないコードであり、取り除いてもプログラムの動作は変わらない。たとえば、次のようなコードだ。

for x in [1,2,3] do
end

アノテーションのコンセプトでは、こういった種類のコードを探し、次のようにアノテートすることができる。:

node.metadata(:dead_code, :true) 

けれども、コードに不要と印をつけるのは、ほんの第一歩に過ぎず、今度は除去する必要がある。ここへ来て、解析と処置の分割が合理的であることが簡単に理解できるだろう。IDEは若干のコードを不要コードとして強調表示したいだけかもしれないが、簡単なコード削除方法(応急処置)を提供することもある。

このコードをどうすれば削除できるのか? もちろん、ASTからノードをダンプした後にRuby2Ruby(source)を使ってRubyソースを吐かせることは可能であろう。しかし、とても気持ちの良いソリューションとは言えない。Ruby2Ruby がParseTree ASTをRubyソースコードに変えるが、フォーマット(空白スペース)やコメントのなどの多数の有用情報を失ってしまう。こうした情報を無くしてしまうことは、IDEやリファクタリング/クリーンアップツールで容認できるものではない。

メタデータの助け船:ノードはソースの場所、つまり、この特定ノードが元々のソースコードで見つかる場所でアノテートすることができる。そしてそれがあれば、ことは簡単である。クリーンアップコードは単にコード中を走って、:dead_codeと印のついた全ノードを見つけ出し、ソース場所メタデータの文字オフセットを使ってソースファイルで印つきのノードを除去する(もちろん、ノードを除去すると、残存ノードのオフセットに狂いが生じる。これを解決するには、削除した文字数を覚えておいて、実際のオフセットから差し引けば良いだけである。こうすれば、ソースの再パーズと再解析は必要ない)。

結論

この記事の例は言語ツールに焦点を当てたが、ここで述べたアイデアはあらゆる種類のオブジェクトコレクションで利用できる。オブジェクトグラフにアノテーションが必要な場所なら、単独で書かれたアナライザによる複数のパスという可能性もあり、その場所がどこになってもノードと一緒にアノテーションを保管しておけば好都合である。オブジェクトグラフのクラスが開発者の制御下になく、他の場所で広範囲に使われるのなら(たとえば ParseTreeにおける配列クラス)、シングルトンクラスを選択すれば良いだろう。その他の状況では、オープンクラスがより良いソリューションとなるかもしれない。

ここで述べた特定ツールの実装に関して:ParseTreeは、Rubinius とJRuby(jparsetree)(サイト・英語)で、Ruby 1.8.x向けに利用可能である。JRuby版は個々のノードにもソースの場所を付加するので、ソースの修正がさらに容易になる。 ツールのアイデア出しやツール書きを楽しもう。Rubyでは簡単にできるのだから。

原文はこちらです:http://www.infoq.com/articles/prototypes-for-metadata
このArticleは2007年12月5日に原文が掲載されました)

この記事に星をつける

おすすめ度
スタイル

BT