BooをDIコンテナのDSLとして使う

BooはDSLの構築に適しているという評判なので、そのへんの事を確かめてみる。

そもそもDSLとはDomain Specific Languageドメイン固有言語)の略で、C#なんかのプログラミング言語が汎用的なのに対して、特定のドメイン(領域)に特化した言語のことをいう。

例えば、最近巷で話題のSilverlightでUIを記述するマークアップ言語であるXAMLもその一つ。本来SilverlightでのUIの構築はC#VBで行うようになっているけど、コードで記述するのはあまりに冗長で見通しが悪いので、XAMLのような宣言型の形式を取ることでその部分を解決している(冗長なのは変わりないけどね)。

まぁ、要するにコードで書くのが面倒くさいから、なんかもっとわかりやすい表現にして楽しようということだ。

で、DSLがあると便利なもので真っ先に私が思い浮かべるものは「DIコンテナ」だ。

普段Spring.NETを使っているんだけど、こいつのオブジェクト定義ファイル(XML形式)は少し大きなアプリケーションになると一気にふくれあがる。これはXMLが冗長なフォーマットだということに一因がある。

なら、「オブジェクト定義ファイルに替わるDSLを作ってしまえばいいんじゃね?」ということで、BooでSpring.NETのオブジェクト定義ファイルの役割を担うDSLを作ってみることにした。

仕様

このDSLのイメージはこんな感じ

context コンテキスト名:
    object オブジェクト名 as 型:
        inject プロパティ名: 値
        ....
    ....

「context」でApplicationContextを宣言し、その中にオブジェクトを宣言していく。
「object」でオブジェクト名とその型を宣言し、その中にはインジェクトするプロパティを宣言していく。
「inject」でプロパティ名とインジェクトする値を宣言する。

なんか、Booで書いてもあまり変わらない気もするけど、まぁやってみよう。

マクロの基本

BooでDSLを構築するには主に「マクロ」という仕組みを使う。これはCやLISPなんかと似たような感じで、コンパイル時に違うコードに展開されるというもの。

マクロを定義するには「AbstractAstMacro」というクラスを継承する。

import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast

class HogeMacro(AbstractAstMacro):
    override def Expand(macro as MacroStatement):
        pass

「AbstractAstMacro」クラスには「Expand」というメソッドだけが定義されているので、これをオーバーライドする。

これは使うには以下のようにクラス名から「Macro」という文字列を省いて、頭を小文字にした単語を宣言する。

hoge "hello"

このままでは何も表示されないので、hogeマクロに渡された文字列を標準出力に出力するようにしてみる。

import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast

class HogeMacro(AbstractAstMacro):
   override def Expand(macro as MacroStatement):
        # 引数のチェック
        if len(macro.Arguments) != 1: return
        
        return ExpressionStatement(
            MethodInvocationExpression(
                ReferenceExpression("print"), macro.Arguments[0]
            )
        )

まず、マクロに渡された引数の数をチェックしている。引数はマクロ名の後に渡された「"hello"」というやつのこと。複数渡す場合はカンマ区切りで渡す(スペース区切りでは無理)。

その後は、馴れない人にはかなりややこしいが説明していくと、

  1. 「ReferenceExpression」クラスを使って「print」関数への参照(実際にはその式)を取得する。
  2. 「MethodInvocationExpression」クラスを使って、第一引数に渡された関数(ここではprint関数)を呼び出す(式を作る)。第二引数にはこの関数に渡す引数を指定する。
  3. 「ExpressionStatement」クラスで「MethodInvocationExpression」をステートメントとしてラップして返す。
  4. 「Expand」メソッドの返り値としてステートメントを返すことによって、元のマクロの式とこの式が入れ替わる。

と、こんな感じになる。

こういった構文木をいじくり倒すプログラムはC#3.0で触ったことがあるので、なんとか理解できたが、こういった経験がない人にはさっぱりだと思う。

まぁこの辺を踏まえてSpring.NET用のDSLを作ってみる。

実装

使用するSpring.NETのバージョンは「1.1.0」

モジュールを以下のサイトからダウンロードしてくる。

ダウンロードしたZIPを解凍したらできるファイルから以下のファイルを作業するフォルダにコピーしておく。

  • antlr.runtime.dll
  • Common.Logging.dll
  • Spring.Core.dll

で、完成したのが以下

SpringMacros.boo

namespace Spring.Macros

import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
import Boo.Lang.Compiler.Ast.Visitors

import Spring.Core
import Spring.Context.Support
import Spring.Objects
import Spring.Objects.Factory.Support

# spring.netのコンテキストを宣言するマクロ
class ContextMacro(AbstractAstMacro):
   override def Expand(macro as MacroStatement):
       if len(macro.Arguments) != 1: return
        
       refExpr = macro.Arguments[0] as ReferenceExpression
       if refExpr is null: return
        
       # アプリケーションコンテキストのインスタンス化
       ctorExpr = MethodInvocationExpression(
           ReferenceExpression("GenericApplicationContext")
       )
       block = Block()
       # 代入と変数宣言
       block.Add(BinaryExpression(
           BinaryOperatorType.Assign, refExpr.CloneNode(), ctorExpr
       ))
       for blk as Block in macro.Block.Statements:
           es = cast(ExpressionStatement, blk.Statements[-1])
           hash = cast(HashLiteralExpression, es.Expression)
            
           for i in range(0, len(blk.Statements)-1): block.Add(blk.Statements[i].CloneNode())
            
           block.Add(MethodInvocationExpression(
               MemberReferenceExpression(
                   refExpr.CloneNode(), "RegisterObjectDefinition"
               ),
               hash.Items[0].First.CloneNode(), hash.Items[0].Second.CloneNode()
           ))
            
       return block
        
# spring.netのオブジェクトを宣言するマクロ
class ObjectMacro(AbstractAstMacro):
   override def Expand(macro as MacroStatement):
       if len(macro.Arguments) != 1: return
        
       tcExpr = macro.Arguments[0] as TryCastExpression
       if tcExpr is null: return
        
       refExpr = cast(ReferenceExpression, tcExpr.Target)
       # オブジェクト定義への参照
       objDefExpr = ReferenceExpression("__${refExpr.Name}__")
        
       block = Block()
       # 代入と変数宣言
       block.Add(BinaryExpression(
           BinaryOperatorType.Assign,
           objDefExpr,
           # オブジェクト定義をインスタンス化
           MethodInvocationExpression(
               ReferenceExpression("RootObjectDefinition"), ReferenceExpression(tcExpr.Type.ToString())
           )
       ))
       for es as ExpressionStatement in macro.Block.Statements:
           pvExpr = MethodInvocationExpression(
               MemberReferenceExpression(
                   objDefExpr.CloneNode(), "get_PropertyValues"
               )
           )
           block.Add(MethodInvocationExpression(
               MemberReferenceExpression(pvExpr, "Add"), es.Expression.CloneNode()
           ))
            
       # オブジェクト名と参照でハッシュを生成する。
       block.Add(HashLiteralExpression(
           ExpressionPair(StringLiteralExpression(refExpr.Name), objDefExpr.CloneNode())
       ))
       return block
        
# オブジェクトのプロパティを宣言するマクロ
class InjectMacro(AbstractAstMacro):
   override def Expand(macro as MacroStatement):
       if len(macro.Arguments) != 1 or len(macro.Block.Statements) != 1: return
        
       refExpr = macro.Arguments[0] as ReferenceExpression
       if refExpr is null: return
        
       valExpr = macro.Block.Statements[0] as ExpressionStatement
       if valExpr is null: return
        
       return ExpressionStatement(
           # PropertyValueを生成
           MethodInvocationExpression(
               ReferenceExpression("PropertyValue"),
               StringLiteralExpression(refExpr.Name), valExpr.Expression.CloneNode()
           )
       )

簡単に説明すると、以下の三つのマクロで構成されている。

  • ContextMacro
  • ObjectMacro
  • InjectMacro

「ContextMacro」はSpring.NETのアプリケーションコンテキストを生成するマクロ。
「ObjectMacro」はそのコンテキスト上にオブジェクトを宣言するマクロ
「InjectMacro」はオブジェクトのプロパティにオブジェクトをインジェクトするマクロ

これを使うためには、まずこのソースをコンパイルしてDLLにする。

PS > booc.exe "-t:library" "-o:Spring.Macros.dll" SpringMacros.boo

そして、このDLLを参照して以下のように記述する。

Usage.boo
import Spring.Core
import Spring.Context.Support
import Spring.Objects
import Spring.Objects.Factory.Support

import Spring.Macros

class Person:
   [Property(FirstName)]
   _firstname as string
   [Property(LastName)]
   _lastname as string

   override def ToString():
       return "${LastName} ${FirstName}"

# オブジェクト定義 DSL
context ctx:
   object hoge as Person:
       inject FirstName: "Hoge"

   object fuga as Person:
       inject FirstName: "Fuga"

hoge = ctx.GetObject("hoge")
fuga = ctx.GetObject("fuga")

print "hoge=${hoge}, fuga=${fuga}"

「Person」というクラスを定義し、それを「ctx」というコンテキストで「hoge」「huga」というオブジェクトを宣言している。そしてそれぞれのプロパティに適当な値をインジェクションしている。

これを実行すると、

PS > booi.exe Usage.boo
hoge= Hoge, fuga= Fuga

と、表示される。

マクロ部分は実際には以下のようなコードに変換されて実行されている。

ctx = GenericApplicationContext()
__hoge__ = RootObjectDefinition(Person)
__hoge__.get_PropertyValues().Add(PropertyValue('FirstName', 'Hoge'))
ctx.RegisterObjectDefinition('hoge', __hoge__)
__fuga__ = RootObjectDefinition(Person)
__fuga__.get_PropertyValues().Add(PropertyValue('FirstName', 'Fuga'))
ctx.RegisterObjectDefinition('fuga', __fuga__)

う〜ん、苦労した作った割にはあまり意味がないですね!!でもおもしろい。