「インスタンスごとのメタクラス」の活用法
こないだLispBuilderを作っていて発見?したテクニックを1つ紹介します。
前提
Lispでは、データ構造の最小単位を「アトム」と呼びます。たとえば、
- "A" ・・・文字列アトム
- a ・・・シンボルアトム
- 12 ・・・整数アトム
- 3.14 ・・・浮動小数点数アトム
などがあります。数値アトムや文字列アトムは、Javaでいう数値プリミティブの値や文字列インスタンスに相当します。シンボルアトムは、変数名や関数名などを表現します。Rubyでは「:hoge」みたいに扱うものです。
(setq a 1)
であれば、setq, aがシンボルアトム、1が整数アトムです。
内部表現をどうするか
これらをLispBuilderのLispの内部表現としてどう扱うかについてですが、手抜きしたいので、整数アトムはjava.lang.Integerのインスタンスとして格納したい。浮動小数点数アトムはjava.lang.Doubleのインスタンスにしたい。それらをwrapするような「Atomクラス」を増やすと、javaやGroovyにデータを受け渡す際に相互変換(box/unbox)が必要になります。透過的にしたい。wrap不要にしたい。
問題点
で、困ったのは文字列アトムとシンボルアトムの扱いです。両者の扱いの違いは、いったん読み込んでしまった後には、評価(eval)時に文字列として扱うか、変数のバインディング(ハッシュ)から変数名をキーとして検索して対応する値を取り出すか、ぐらいの違いでしかない。ので両方ともStringにしたいところです。でもそれだと両者を区別できない。
まあ確かにシンボルアトムをたとえばSymbolAtomというクラスとかで扱うのはやぶさかではないのですが、そうすると、文字列はStringAtomに,整数はIntegerAtomにし実数はDoubleAtomにしたくなる。んでNumberAtomをはさんで共通の親クラスとしてAtomクラスを作りたくなる。AtomとCons Pairの共通の祖先クラスListが欲しくなり・・とどんどん型が増えていってしまう。まあJavaならそうなるでしょうし、そうするしかない。でもそれってGroovy流ではないんですよ。私見ですけどね。前述の透過性の問題もあるし、クラス数は増やしたくない。
解決策
で考えた結果、まずは静的イニシャライザ(LispList.groovy)で、クラスごとのメタクラスを用いて
String.metaClass.getIsSymbol = { false }
という常にfalseを返すクロージャをStringのメタクラスのisSymbolプロパティのgetterとして注入しておきます。この結果、
assert "abc".isSymbol == false
となります。次に、Lispコードを読み取るときにシンボルアトムを生成するとき、
def newSymbolAtom = new String(s) newSymbolAtom.metaClass.getIsSymbol = { true }
とやって、Stringの「インスタンスごとのメタクラス」に対して、trueの値を返すクロージャをisSymbolプロパティのgetterとして注入しておきます。
なお、上でわざわざnew String()してるのは、internされて同じStringインスタンスをシェアしてしまう可能性があるためです。
さてこれで、文字列として扱うことができるけれども、isSymbolが真を返すようなシンボルアトムの出来上がりです。Stringインスタンスをeval際には、isSymbol==trueなら変数バインディングからその文字列をキーとした変数値を取り出し、isSymbol==falseなら文字列そのままの値を返せば良いです(この処理もLispList.groovy中でString.metaClass.evalに注入しています)。
まとめ
groovy 1.6で機能追加された、「(Javaで定義されたクラスのインスタンスに対しても適用可能な)インスタンスごとのメタクラス」機能を使えば、JavaのクラスであるStringクラスにインスタンスごとに機能を追加することができ、コードを簡潔にすることができる場合があります。
参考
- 作者: 浦沢直樹,手塚治虫,長崎尚志
- 出版社/メーカー: 小学館
- 発売日: 2009/06/30
- メディア: コミック
- 購入: 14人 クリック: 101回
- この商品を含むブログ (232件) を見る
完結しましたね!