uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

「インスタンスごとのメタクラス」の活用法

こないだ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クラスにインスタンスごとに機能を追加することができ、コードを簡潔にすることができる場合があります。

参考


PLUTO 8 (ビッグコミックス)

PLUTO 8 (ビッグコミックス)


完結しましたね!