モチベーション
次のプログラムはコンパイル時エラーになる。
staticでないフィールド変数の初期化には定数式、staticメソッド、staticフィールド等しか使えない。この制限は Boo ではない他の C#や Java 言語などにもある。
# pythonじゃないよ!Booだよ!
class Hoge:
v = init(5) # フィールド変数の初期化にインスタンスメソッド使っちゃだめだよ!
def init( n as int ):
return n
どうしてもやりたければ、次のように書けばいいが、たかが初期化式程度のためにこれはメンドクサイ。
class Hoge:
private cache = null # v の値を保持する。最初は null。
v: # v へのアクセスはメソッド経由(プロパティ)にする
get:
if cache is null:
cache = init(5) # こんなところに初期化式が...
return cache
set
cache = value
これをマクロで自動化する。
ネタ元
Boo コンパイラソースのtests/testcases/macros/type-member-macro-yielding-member-with-member-generating-attribute.boo に同じようなサンプルがあるが次の理由でもう少し修正がしやすくて、見通しのいいものにしてみた。
- サンプルは属性を使うので、修正には属性プログラムのコンパイル→目的プログラムのコンパイル、と2段階必要。
- サンプルは、getter しかなくて setter がなかった(上記変数 v への代入ができない)。
解決方法
次のマクロ deferred を使う。
# pythonじゃないよ!Booだよ!
# フィールド宣言の前につけて使用します。
macro deferred:
# "deferred '変数名' = 式" という記述にマッチするパターンマッチングブロック。
# 変形は上記の property を使った例とほぼ同じ。
case [| deferred $(ReferenceExpression(Name: name)) = $initializer |]:
# Context.GetUniqueName() は一時変数名等を作るのに使用出来る、
# freshな名前を生成するメソッド。
cachevar = ReferenceExpression( Context.GetUniqueName( 'tmp' ) )
g = Method()
g.Body = [|
if $(cachevar) is null:
$(cachevar) = $(initializer)
return $(cachevar)
|]
s = Method()
s.Body.Add( [| $(cachevar) = value |] )
yield Property( Name: name, Getter: g, Setter: s )
yield [|
private $(cachevar) = null
|]
このマクロは次のように使う。
class Hoge:
deferred v = init(5) # init() メソッドつかえまーす
def init( n as int ):
return n
注意点
変数に null を代入(設定)すると、次の取得時に再度初期化値が代入されてしまう(解決はできるけどもう一つフラグが必要です)。null を代入したら初期化式で初期化されるというのはある意味おもしろい機能ですけどね...。
結論
Context は macro 定義内部で使える変数で、コンパイラの動作状況を保持しています(型は CompilerContext)。
Context.GetUniqueName() は大掛かりな構文木追加/修正をするのに便利です。