2015の23日目の記事です。qiita.comすごく及び腰でしたが、ずっと空いていたので、えいやで登録してみました。マクロ展開時に副作用を起こすな危険、という内容です。値を返す他には周囲の世界に影響しようとすべきではない.」とあります。下線を言い換えると副作用を起こすなということになると思います。触る以下の例が示されています。nil))正直に言うとその下にある説明では結局いつ困るのかピンと来ませんでした。Parenscriptをいじっていてこれで散々ハマった*1ので、勉強結果を展開してみます。次を伝えることが目標です。大多数のLisperにとっては分かりきった話だろうと思いつつ、次のような感じで進めていきます。Lispの(サブセット)コードをJavaScriptコードに変換してくれるライブラリです。Lispコードを書くとJavascriptコードを文字列として出力してくれます。20);"Lispとしては外せないマクロもサポートされていて、大きくは次の2つの方法で定義できます。30;"ps環境内でのdefmacroは内部的にはdefpsmacroを呼んでいます。このため、どちらも同じように使えます...だったら良かったのですが...。何が起きるのかParenscript用のマクロ(以下、PSマクロ)定義を2種類紹介しました。これらはPSマクロを管理するグローバルな変数parenscript::*macro-toplevel*に登録されるタイミングが異なります。方が展開時に副作用を起こすというまずい動作をしています。これが何を引き起こすのか見てみます。準備REPLや一つのスクリプトファイルで試していても中々起きない現象であるため、小さなプロジェクトを一つ起こして、quicklispの配下に置きます。編集して次のように2種類の方法でParenscript用のマクロを定義してみます。(test-defpsmacro)))の部分でマクロが動きませんが、本題ではないので詳細略です*2。gist.github.comさらに、できるだけクリーンな環境で実行したいので、Roswellスクリプトを一つ起こして、上記のprint-ps関数を呼び出すコード(とParenscriptをロードするコード)を追加します。execute.rosgist.github.com実行さて実行です。test-defmacro-in-ps';"どちらの定義もパッケージ内部ではうまく動いているようです(下3行)。次はパッケージ外(execute.ros側)から呼び出すため、以下を修正します。(fresh-line))そして実行。test-defmacro-in-ps';"なんだ問題ないじゃないか...と思って、もう一度実行してみます。test-defpsmacro';""testDefmacroInPs();"なんということでしょう。1回目と異なり、execute.ros側だけps環境内のdefmacroで定義したtest-defmacro-in-psマクロが消えています(関数扱いされています)。再現条件に確信を持てないため、色々いじっても直ったのか判断できない見た瞬間デバッグする気力が削られる要素に満ちています。解説どうしてこうなったかを解説し、さらに、現象を再現する小さなコードを書いて動作を眺めてみます。してこうなった上記の2回連続実行の出力を良く見ると、本体であるprint-psの出力の手前に違いがあります。test-ps-eval-order]の出力がありますが2回目はありません。quicklisp/setup.lispを見てみると、これはコンパイル時のみ出力されるメッセージのようです*3。name))))ここから、以下の違いにより1回目と2回目で結果が変わったと推測できます。マクロ展開時の副作用はバイナリには残らないことに注意します*4。test-defmacro-in-psマクロの定義がexecute.rosからは見えない結局のところマクロ展開時の副作用は、ライブラリに変更がない場合はコンパイルを省略しても結果は変わらない、という(妥当な)仮定を崩すことになります。その他2点の疑問は以下のように説明できます。parenscript::*macro-toplevel*への登録)を行う処理自体が残るので、ロード時に定義が実行されるため小さく再現してみる解説のためというよりは、現象をより剥き出しにするための小さなコードを書いてみます。2つのファイルを用意します。1つはライブラリのつもりでtest-lib.lispを、もう1つはこれを利用するアプリケーションのつもりでtest-app.rosスクリプトを用意します。hoge)))外の環境に触れたくなるのは大抵define系マクロだろうと思い、test-lib.lispではdefhogeというhogeを定義するマクロを提供します。defhoge-wrongも同様ですが、マクロ展開時に登録を行うという間違った動作をします。それぞれを利用して2つのhoge、libとlib-wrongを定義します。hogeを登録順に出力するprint-all-hoge関数も提供します。(fresh-line))test-app.rosでは上記のtest-lib.lispをロードして2つのhoge、appとapp-wrongを定義し、print-all-hogeを呼び出して登録済みhoge一覧を出力します。ロードは*load-kind*の値に応じて3種類のいずれかの方法で行います。APP-WRONGここまでの説明で原理は分かるはずなので解説は省略します。念のため、最初のtest-ps-eval-orderの例では、1回目の実行は*load-kind*が1の場合に、2回目の実行は*load-kind*が2の場合に相当します。この例では*load-kind*を固定している限り、何度実行しても実行結果は変わりません。余談ですが、hogeを記録する*hoge-func-list*をdefvarではなくdefparameterで定義すると、*load-kind*が1のときの結果が2の場合と同じになります。定義済みの変数を上書きするdefparameterと上書きしないdefvarの挙動の違いですね。最後にこの記事の発端となったコードを。Parenscriptをもう少し便利に使えないかと色々実験をしているps-experimentというライブラリを作っています*6。github.comこの中で、ps環境下で利用できるdefstructのサブセットを作ったのですが、これを利用するコードでエラーが出て散々にハマりました。定義の一部を載せます。冒頭のパース系の関数の定義は本題と無関係なため省略します。gist.github.comコメントにありますが、アクセサの定義で利用しているdefmacroが問題です。ここで注意ですが、defpsmacroで定義したParenscript用のマクロは、結局ps環境下で展開されます。このため、defpsmacro下でのdefmacroはps環境下でのdefmacroと実質上同じものです。アクセサがマクロ展開時に定義されてしまうため、このdefstructで定義した構造体を別のライブラリから使おうとすると、アクセサだけ見えない(ことがある)という問題に悩まされることになります(なりました)。厄介なことに、この問題はps-experimentのテストでは検出されませんでした。テストでは同じ環境下でdefstructによる構造体定義とそのテストコードをロードするため、問題なく「動いてしまいました」。上記のtest-libとtest-appの例で言うと、test-app側でのdefhoge-wrongの利用に相当するケースです。結局どうしたのかですが、ps環境下でのdefstructはデサポートすることにしました。代わりに、これをラップしてトップレベルで利用するために用意していたdefmacro.psマクロを直接提供することにしました。gist.github.comdefxxx.ps系マクロはトップレベルでParenscript用の色々を定義するためにps-experimentで用意しているマクロ群です。全体として、defpsmacroをdefmacroで、その他defxxxをdefxxx.psで置き換えた以外、見た目に大きな違いはありません。マクロ展開時にグローバルな値を読み込んでいる箇所があり、問題がないか気にしています。具体的には、include(スロット定義の継承)を実現するために、parse-defstruct-name-and-optionsが*ps-struct-slots*(上記では省略)というグローバルなハッシュを読み込んでいます。このハッシュへのスロットの登録はregister-defstruct-slotsで行っています。eval-whenによる指定で、下記のようなコードをコンパイルしたときにも、childのマクロ展開よりも早い段階でparentの登録を行う形になっています(かつマクロ展開時の副作用を避けています)。ここのハッシュ読み込みは明らかに「純粋に関数的」でないため怪しいのですが、当面これで様子を見ようと思っています。c)現状で見えている怪しげな動作というと、コンパイル→ロードとすると同じ定義が2度実行されるというものがあります。同じもので上書きするだけなので大抵問題ない...はず。LIB"が2回出ます)。envに一時的に登録することで副作用を避けているようです(define-compile-time-structure)。マクロ展開が「純粋に関数的」な動作をするようにかなり慎重に作られている様子が伺えます。注意ですが、まだ&environmentを理解し切れていないので嘘を言っているかもしれません。まとめ:恐しさについて改めてマクロ展開時に副作用を起こすことの恐ろしさは、原因を特定しにくいバグにつながる、というところにつきます。マクロ展開時の副作用の結果は環境には残るため、Lispの利点であるインクリメンタルな開発の最中にはまず気づきません。テストを書いてクリーンな環境で実行していてもまだ気づかないケースも多いです。これは、上記のps-experimentのdefstructサブセットのように、自身では使わない外向けに提供する機能で起こりやすいです。ある日、実行条件に応じて結果が変わるような再現しにくいバグに遭遇します。バグの原因特定を困難にする典型的な要因である、発見までに時間がかかることと、再現条件が分かりにくいことという両方を満たすわけです。自分はこのバグに遭遇してから見当違いの方向にも走りつつ数日苦しみました。いうことで、マクロ展開時の副作用には敏感になりましょう、と釈迦に説法をしたところで終わりにします。*1:Parenscriptの仕様と関係なく勝手にハマった部分も多々ありますが、、とりあえず関係ある部分の紹介です*2:eval-whenがdefpsmacro内で呼ばれていないのは、それはそれで問題なのですが、外付けで対処可能なため傷は浅いです。Overflow」でしょうか。Parenscriptのこのコミット(リンク)でmasterは修正されていますが、quicklispの参照しているhttp://common-lisp.net/project/parenscript/release/parenscript-latest.tgzに反映されていないようです*3:本題と関係ない調査メモ。HyperSpecによると、*compile-file-pathname*はcompile-file関数の実行中のみファイルパスが設定され、それ以外はnilにセットされるもののようです。macroexpand-progress-fun自体は、*macroexpand-hook*用のhook関数を返します。切り出したコードの少し下を見ると、defpackageマクロの展開時であることが内部関数show-packageを呼び出す条件の一つになっています。以上を合わせて、コンパイル時のみパッケージ名を(重複なく)出力する動作を実現しているようです。macro"と記述されています。201810日目の記事です。2016の20日目の記事です。してみる月別アーカイブ参加グループプログラミングはてなブログをはじめよう!eshamsterさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。