世にはびこる辛さの原因の1つは暗黙の参照関係($loadの挙動を変えた)
現状の$loadのscope
現状の$loadの実装はclosureと同様の形で外側のscopeの値に触ることが出来る。これが実質dynamic scopeのような形になっている。これが微妙かもしれない。
例えば、こういう分からない変数名fooの束縛を持つ環境でloadを実行すると、sub.yaml中のdescription部分でmain.yamlのfooが参照される。
# main.yaml $let: foo: bar body: $load: ./sub.yaml
# subm.yaml name: sub description: {$get: foo}
外側のscopeに内側のscopeから触ることができるのでこれは動く。
$ zenmai main.yaml name: sub description: bar
$loadには引数を渡す事ができる
呼び出し元と呼び出し先でのインピーダンスミスマッチ?の解消のために$loadは引数を受け取る事ができる。例えば、トップレベルではnameという名前の束縛だったものを、load先の環境ではtitleとして利用したい。こういうことはたまにある。これを解決するために、$loadは引数を受け取れる。
# main2.yaml $let: name: foo body: {$load: sub.yaml, title: {$get: foo}}
# sub2.yaml # need: title title: {$get: title}
$ zenmai main2.yaml title: foo
暗黙の参照は良くない
暗黙の参照は良くない。何だかわからない前提知識を要求されてしまう。そしてエラーになったときに辛い。例えば、N段のネストが続いた$loadの末端でエラーが発生したときについて考えてみる。例えばエラーの原因はtypoだったとする。この時に、暗黙の参照を当てにした書き方であったならば、N-1段全て確認する必要がある。一方で全ての引数が明示的に渡されてくる記述の場合には、1つ前の段を確認するだけで良い。
また、そもそも引数として渡される量と内部で参照して使われる束縛の数が一致しない場合に、不用意に依存が多いような構造をそのままに見逃してしまう。これもあまり良くない。そんなわけでそもそもloadは引数を取れるのだからscopeを受け継いでloadする仕組みをデフォルトにするのは良くなさそうな気がする。
そんなわけで$load時のscopeを変更した
今度からは$loadを呼び出した時にはトップレベルの環境からscopeが作られることにした。なので冒頭のfooの参照はエラーになる(エラーメッセージを親切にしなければいけないという別のタスクは存在する)。明示的にfooを渡すかとても長く不格好な名前のload_with_dynamicscopeを使えば冒頭の例は動くようになる。
# main3.yaml $let: foo: bar body: {$load: ./sub.yaml, foo: {$get: foo}}
もしくは
# main4.yaml $let: foo: bar body: $load_with_dynamicscope: ./sub.yaml
ところで
$getを書くのがわりとだるい。 {$get: foo}
という感じで {
と }
で囲まなければいけないのがわりとだるい感じ。$以外にちょうど良いprefixがないのであれだけれど。 "@foo"
みたいな記述を許すと便利かもしれない?
こういうちょっとした切り替えはmonkey patchで無理矢理行う事もできて。
from zenmai.compile import Evaluator def eval(self, context, d): if hasattr(d, "keys"): return self.eval_dict(context, d) elif isinstance(d, (list, tuple)): return self.eval_list(context, d) elif str(d).startswith("@"): return self.eval(context, {"$get": d[1:]}) else: return d Evaluator.eval = eval
以下の様なコードを書いてあげて、冒頭でimportしてあげれば大丈夫。
$let: foo: bar _: {$import: ./patch.py} body: {$load: ./sub.yaml, foo: "@foo"}
うーん。書く分にはこちらの方が良い気がするのだけれど。