引数文字列をinternするアナフォリックマクロでハマったこと

CSVファイルを処理するために、引数の文字列をシンボル化して(intern)束縛するアナフォリックなマクロを書いていてハマったことのメモ。シンボルとパッケージの関係は段々と分かってきたつもりでしたが、まだまだハマるときはハマりますね。

悪い例

例のための例ですが、まずパッケージAで目的のマクロwith-interned-strを定義します。すぐ下の使用例を見ると分かる通り、第一引数の文字列("abc")をinternしてできあがったシンボル(ABC)に、第二引数の値(100)を束縛し、body内では変数のように扱えるという代物です。

次に、パッケージBではwith-interned-strを利用するマクロsome-macro*1を定義します。実際に下でsome-macroを試してみると、意図通りシンボルDEFに値が束縛されており、渡した200という値が出力されます。

よしよしと思って今度はパッケージCからsome-macroを呼び出してみると、PACK-B::DEFが束縛されていないと怒られてエラーになります。with-interned-strの引数strはパッケージCのコンパイル時にinternされる一方で、some-macroの(print def)のdefはパッケージBのコンパイル時にinternされているためです。つまり、前者はPACK-C::DEF(束縛あり)、後者はPACK-B::DEF(束縛なし)と全く違うシンボルなのでエラーになったということでした。

なお、このテストコードは余りにも意味のない例ですが、実際にはCSVのヘッダ名を渡して同名のシンボルに値を束縛して…というマクロを書いていました。

割と大丈夫な例

パッケージ間で共通して使えるシンボルというとキーワード(:str もしくは (intern "str" "KEYWORD"))が思い当たるのですが、値を束縛できないので今回は使えません。とりあえず両者が同じパッケージでinternされていることを保証できればいいので、body内に同名のシンボルを見つけたらstrをinternしたもので全て置き換えるという力技で解決してみました。

sublis関数でsymをsymで置き換えるという一見怪しげなことをしていますが、test関数にパッケージを無視してシンボル名のみを比較するequal-nameを指定しているため、これで目的の動作が得られます。この手のtestキーワードを受け取る関数について深く考えたことがありませんでしたが、こう見ると応用範囲は結構広そうです。

大丈夫でない部分があるので「割と」とつけましたが、気付いた問題だけ書いていきます。明示的にパッケージを指定したシンボルまで置き換えてしまうので、わざわざパッケージ名をつけて書いたのに上書きされてしまうという、分かり辛いバグを仕込む可能性があります。そうそう困らなさそうですし、いい解決策がすぐに思いつかなかったので妥協していますが何かいい解決法はあるのでしょうか…。

また、デフォルトの関数名やマクロ名とかぶると(特に後者の一部は)回避手段がないのでは…と思ったのですが、名前空間が分かれている恩恵なのか下のようなコードも問題なく動きました(他にマクロ"loop"やスペシャルフォーム"let"も大丈夫でした)。まだ理解不足できちんと説明できないのですが、use, importしているものは問題なさそうです。

(with-interned-str "print" 100
  (print print))

原因不明でハマったこと(原因不明で直ったこと)

再現性はないですが、もともとハマったコードの方では実行後にSBCLの環境に何やらゴミが残ったらしく、リスタートしても該当のシンボル(上記の例でいえばPACK-B::DEF)がunboundだとエラーで怒られ続ける現象が起きました。上記のような解決方法が間違っていると思い試行錯誤していたので中々気付かなかったのですが、ちょうど気付いた辺りでなぜか直ったので結局なんだかよく分かりませんでした。

明示的に直すのであればunintenすれば良いのかもしれません。

*1:名前を考えるのが面倒になったわけではありますん