Scala

Scala macros中級者の壁: symbolとowner

More than 1 year has passed since last update.

Scalaのマクロには、ある程度複雑なマクロを書こうとしたときに立ちはだかる壁があります。それがsymbolとownerです。この壁の厄介なところは、ほぼ確実に引っかかる罠でありながら英語も含めてドキュメントがほぼゼロという点です。これについて簡単に解説したいと思います。


Symbol

Scalaのマクロにはsymbolという概念があります。12 Symbolは、変数・メソッド・クラスなどの識別子同士を区別したり、メンバ等のメタ情報へのアクセスを仲介したりするために使われます。Cの字句マクロやLispの構文マクロなどにおいては名前被り・名前空間の汚染等を防ぐこと、一言で言うとhygiene性が大きな問題となっていますが、Scalaのマクロにおいては同様の問題は起きにくくなっています。3 なぜなら、Scalaのtyper(より厳密にいうとnamer)が各定義・参照の構文木に適切なsymbolオブジェクトを割り付けてくれるため、同じ名前の識別子があっても簡単に区別できるからです。4


ハマりどころ

この親切心(?)ですが、ある程度複雑なマクロを書くときには逆に仇となります。例えば、以下のように変数宣言を含むコードをコピーするマクロを考えましょう。何が起こるでしょうか?



val a = 10

println(a)



{

val a = 10
println(a)
}
{
val a = 10
println(a)
}

普通に作ると2つの変数aが同じsymbolを参照してしまい、おかしなことが起きてしまいます。つまり、片方の変数aについて新しいsymbolを生成してsymbolを付け替える必要があります。c.internalに必要なメソッドが用意されているのでTransformApiを使って頑張って再帰的に付け替えていきましょう。substituteSymbolsで多少手抜きできるかもしれませんが、そもそも定義を全列挙しないといけないので割とどうしようもないところがあります。


Symbolを持つ構文木

ところでsymbolを持つ構文木にはどういうものがあるのでしょうか?書いてある文献が全然ないのでここにまとめておこうと思います。



  1. DefTree族: ValDef, DefDefなどDefTreeを継承しているものです。Bindなど名前にDefが入っていないものもあります。


  2. Function: 無名関数です。


  3. Template: クラス定義のbodyです。


  4. RefTree族: Select, Ident などです。これはsubstituteSymbolを使うと気にする必要がないので忘れてもよいです。

その他にもTypeTreeの中にも実は色々あったりしますが、そちらはコンパイラプラグインでも書かないとあまり触れる機会がないので気にしなくてよいです。実のところ私も把握しきれていないので誰かまとめた資料ください。:bow:


小ネタ


Symbolオブジェクトはmutable

Scalaのマクロは基本的に構文木がimmutableになるように設計されています。が、symbol周りはmutableです。定義と参照の両方で共有するものなので当然といえば当然ですが。ついうっかり関係ない変数でsymbolを共有するととても良くわからないエラーに悩まされるので気をつけましょう。


2種類のコピー

右辺だけ書き換えたいなど、変数宣言をコピーするときは、初めは以下のように書いたりすることも多いと思います。

$mods val $name: $tpt = $rhs

しかし、玄人のコードを読むと普通treeCopy.ValDefが使われていると思います。

treeCopy.ValDef(original, mods, name, tpt, rhs)

これは、後者だとoriginalのsymbolが新しいtreeにも割り付けられるからです。前者だと消えてしまい、あとでタイプチェッカを通したときに新しいsymbolが割り付けられます。このため、右辺だけ書き換えたいというようなときに前者のように書くと、symbolの対応関係が崩れて『そのsymbolを持つ変数ないよ』というコンパイルエラーに悩まされることになります。逆にインライン展開など同じ構文木を複製したいときには後者でやってしまうと困ったことになるかも…しれません。どうせ後で書き換えないといけないのですが。


Owner

もう一つ面倒な問題はsymbolのownerです。Ownerとはそのsymbolがどの文脈に出てくるかを示すものであり、他のsymbolへの参照で表されます。例えば以下のようなプログラムを考えます。

class Hoge {

def piyo(): Unit = {
val fuga = 10
}
}

このときfugaに対応するsymbol(面倒なので以下fugaと同一視します)のownerは、piyoのローカル変数なのでpiyoとなります。また、piyoのownerはHogeです。

Symbolのownerが適切に設定されていないと、マクロ展開時点ではエラーが出ませんが、コンパイラの後ろの方のフェーズでコケます。(完全に実装上の手抜きだと思うのですが。。)例えば、以下のようなメソッドが入れ子になっているプログラムを考えます。

class Hoge {

def piyo(): Unit = {
val piyoLocalVal = 10
def fuga(): Unit = {
println(piyoLocalVal)
}
fuga()
}
}

Javaを知っている人ならわかるようにJVMではメソッド内で定義するメソッドをサポートしていないので、Scalaコンパイラでは以下のようにfugapiyoの外に出します。5 このとき、piyoLocalValpiyoの中でだけ通用する変数なので、fugaの追加の引数として渡されることになります。

class Hoge {

def piyo(): Unit = {
val piyoLocalVal = 10
fuga(piyoLocalVal)
}
def fuga(piyoLocalVal: Int): Unit = {
println(piyoLocalVal)
}
}

ここでpiyoLocalValのownerが間違っていると何が起きるでしょうか。たとえば、Hogeになっているとします。するとscalacはpiyoLocalValHogeのインスタンス変数と誤認し、fugapiyoの外に出して相変わらず見えると誤解します。したがって次のようなプログラムを出力してしまいます。

class Hoge {

def piyo(): Unit = {
val piyoLocalVal = 10
fuga(piyoLocalVal)
}
def fuga(): Unit = {
println(piyoLocalVal)
}
}

この結果、あとでfugaから見たとき、存在しないはずのpiyoLocalValを参照してしまっているためうまくコードが出力できずscalacが例外で死んでしまいます。なお、そもそもscalacの設計上想定されていないエラーのため、ユーザーフレンドリーなエラーメッセージは出てくれません。。


正しいownerの付け替え方

このようにownerを付け替えないといけないというのは理解していただけたと思うのですが、ではどう付け替えるのが正しいのかというと…そんな資料はどこにもありませんでした。なのでソースコードを見ることになります。この辺が一番わかり易いのではないでしょうか。

https://github.com/scala/scala/blob/v2.12.1/src/reflect/scala/reflect/internal/Trees.scala#L1204

直感的には外側の構文木で直近のsymbolを持っている木がownerです。が、CaseDefなどownerになれないものが幾つかあります。Ownerになれるものをまとめると以下のようになります。



  1. ClassDef, ModuleDef, ValDef, TypeDef, DefDef の5種。


  2. Function (無名関数)

この原則に従って以下のソースコードのようにsetOwner, changeOwnerを頑張ればあなたもowner chainマスターです!がんばってください。私はこれを調べるために2015年をすべて溶かしたと言っても過言じゃないです。。

https://github.com/hiroshi-cl/InverseFramework/blob/master/dsl/continuations/src/main/scala/inverse_macros/continuations/cpsParam.scala

ところでよく見るとソースコードには嘘が1つあります。Templateはownerになれません。間違えないでください。

[3/1加筆] 中の人に聞いた結果、型なし構文木など一部の場合には Template がownerになることがあるそうです。が、型チェッカを通すとそのような場合は消滅するので上記のように覚えておけば良いです。


自動で修正するプラグイン

ちょっと考えればわかると思いますが、この問題はある程度自動で解決できます。以前簡単に試作したものがこちらにあります。Inverse macro plugin に組み込んでみました。

https://github.com/hiroshi-cl/InverseFramework/blob/master/core/plugin/src/main/scala/inverse_macros/pieces/mixin/RepairOwnerChain.scala

残念なことに Scala 2.12 では動かなくなってしまっているのでそのうち修正したいと思います。

そもそも本体になぜ自動修正、あるいはせめて検証だけでも入っていないのか極めて疑問ですが…


まとめ

この記事を読めばsymbolとownerへの対策はバッチリだと思います。よいScalaマクロライフを!

とはいえ、こんな変なハマりどころがあるのもScalaコンパイラのAPIをハックして無理やりマクロとして使えるようにしているからなので、scala.metaベースの次世代マクロではその辺改善されることを期待しています。。


おまけ: ありがちなworkaroundに対するコメント


untypecheck


  1. 一部のパターンマッチなど幾つかuntypecheckできない構文木がある

  2. 元のソースコードにあった型注釈のうち一部はtyperによってsymbolに移され構文木上から消えるため、untypecheckすると本来必要な型注釈がなくtyperを通らなくなる場合がある


show+parse


  1. カッコを付ける場所がおかしいなど幾つか挙動の怪しいところがある

  2. 特殊なmodifierを<>で出力するなどそもそも仕様上scalaでコンパイルできることを意図した出力をするメソッドではない


  3. -Y系のデバッグ用オプションを付けると出力文字列が変わって動かなくなる





  1. http://docs.scala-lang.org/ja/overviews/reflection/symbols-trees-types 



  2. Scalaにはシンボルリテラル(例:'symbol)というマイナーな機能がありますが、おそらくこれとは関係ありません。 



  3. たまに勘違いしている人がいますが、hygiene性は100%問題が起きないことを保証するものではなく、安全性を保障しようとする何らかの機構を持っているかどうかです。故意に破壊しようと思えば簡単に壊せます。 



  4. 今回の話とは関係ありませんが、必要があればfresh nameを生成できるAPIの方もあります。 



  5. 関数型言語コンパイラではこういう操作をラムダリフティングというらしいです。そのためこの操作をするscalacのフェーズ名としてlambdaliftという名前がついています。