コンピュータサイエンスにおいて、衛生的マクロとは、その展開によって識別子が誤ってキャプチャされないことが保証されているマクロのことである。これらは、 Scheme、[1] Dylan、[2] Rust、Nim、Juliaなどのプログラミング言語の機能である。偶然のキャプチャという一般的な問題は、衛生的マクロが導入される前からLispコミュニティではよく知られていた。マクロ作成者は、この問題を回避するために、一意の識別子を生成する言語機能(gensymなど)を使用するか、難読化された識別子を使用する。衛生的マクロは、マクロ展開器に統合されたキャプチャ問題に対するプログラム的なソリューションである。「衛生」という用語は、数学で使用される用語に触発されて、衛生的マクロ展開を紹介したKohlbeckerらの1986年の論文で造られた。[3]
衛生問題
変数シャドウイング
非衛生的なマクロ システムを持つプログラミング言語では、マクロの展開時に作成される変数バインディングによって、既存の変数バインディングがマクロから隠される可能性があります。Cでは、この問題は次のコード例で説明できます。
#define INCI(i) { int a=0; ++i; }
int main ( void ) { int a = 4 , b = 8 ; INCI ( a ); INCI ( b ); printf ( "a は現在 %d、b は現在 %d です\n " , a , b ); return 0 ; }
上記をC プリプロセッサで実行すると、次の結果が生成されます。
int main ( void ) { int a = 4 , b = 8 ; { int a = 0 ; ++ a ; }; { int a = 0 ; ++ b ; }; printf ( "a は現在 %d、b は現在 %d です\n " , a , b ); return 0 ; }
a最上位スコープで宣言された変数は、マクロ内の変数によって隠蔽され、新しいスコープが導入されます。その結果、コンパイルされたプログラムの出力が示すように、プログラムの実行によって が変更することはありません。
aa
aは現在4、bは現在9です
標準ライブラリ関数の再定義
衛生問題は変数バインディングを超えて広がる可能性があります。次のCommon Lispマクロを考えてみましょう。
( defmacro my-unless (条件&body本体) ` ( if ( not ,条件) ( progn ,@本体)))
このマクロには変数への参照はありませんが、シンボル「if」、「not」、および「progn」はすべて標準ライブラリの通常の定義にバインドされていると想定しています。ただし、上記のマクロは次のコードで使用されています。
( flet (( not ( x ) x )) ( my-unless t ( format t "これは印刷されるべきではありません!" )))
「not」の定義はローカルで変更されており、拡張もmy-unless変更されています。
ただし、Common Lisp では、11.1.2.1.2 準拠プログラム用 COMMON-LISP パッケージの制約に従って、この動作は禁止されていることに注意してください。関数を完全に再定義することも可能です。Common Lisp の一部の実装では、ユーザーが誤ってパッケージ内の定義を変更しないように、パッケージ ロックが提供されています。
プログラム定義関数の再定義
もちろん、プログラム定義関数でも同様の問題が発生します。
( defunユーザー定義演算子( cond ) ( not cond ))
( defmacro my-unless (条件&body本体) ` ( if (ユーザー定義演算子,条件) ( progn ,@本体)))
; ... 後で ...
( flet ((ユーザー定義演算子( x ) x )) ( my-unless t ( format t "これは印刷されるべきではありません!" )))
使用サイトはマクロを再定義しuser-defined-operator、マクロの動作を変更します。
衛生的なマクロがない言語で使用される戦略
衛生問題は、従来のマクロでいくつかの代替ソリューションを使用することで解決できます。
難読化
マクロの展開中に一時的なストレージが必要な場合の最も簡単な解決策は、マクロ内で珍しい変数名を使用して、プログラムの他の部分で同じ名前が使用されないようにすることです。
#define INCI(i) { int INCIa = 0; ++i; }
int main ( void ) { int a = 4 , b = 8 ; INCI ( a ); INCI ( b ); printf ( "a は現在 %d、b は現在 %d です\n " , a , b ); return 0 ; }
という名前の変数INCIaが作成されるまで、このソリューションは正しい出力を生成します。
aは現在5、bは現在9です
現在のプログラムでは問題は解決されていますが、この解決策は堅牢ではありません。マクロ内で使用される変数とプログラムの残りの部分で使用される変数は、プログラマーによって同期されている必要があります。具体的には、INCI変数でマクロを使用するとINCIa、元のマクロが変数で失敗したのと同じように失敗しますa。
一時的なシンボルの作成
一部のプログラミング言語では、新しい変数名またはシンボルを生成し、一時的な場所にバインドすることができます。言語処理システムは、これが実行環境内の別の名前や場所と衝突しないことを保証します。マクロ定義の本体内でこの機能を使用するかどうかを選択する責任はプログラマに委ねられています。この方法はMacLispで使用されており、名前付きの関数をgensym使用して新しいシンボル名を生成できます。同様の関数 (通常は名前も付けられます) は、広く実装されているCommon Lisp標準[4]やElispgensymなど、多くの Lisp 系言語に存在します。
シンボル作成は変数のシャドウイングの問題を解決しますが、関数の再定義の問題を直接解決するわけではありません。[5]しかし、、gensymマクロ機能、および標準ライブラリ関数は、非衛生的な言語に衛生的なマクロを埋め込むのに十分です。[6]
読み取り時の非インターンシンボル
これは、同じマクロの複数の展開で 1 つの名前が共有されるという点で難読化に似ています。ただし、珍しい名前とは異なり、読み取り時に非インターン化されたシンボル (#:表記で示される) が使用され、 と同様に、マクロの外部で発生することは不可能ですgensym。
パッケージ
Common Lisp などのパッケージを使用すると、マクロは、マクロが定義されているパッケージのプライベート シンボルを使用するだけです。シンボルは、ユーザー コード内に偶然出現することはありません。ユーザー コードは、::たとえば というプライベート シンボルを使用する許可を与えるために、二重コロン ( ) 表記を使用してパッケージ内に到達する必要がありますcool-macros::secret-sym。その時点で、偶発的な衛生状態の欠如の問題は意味をなさなくなります。さらに、ANSI Common Lisp 標準では、標準関数および演算子をグローバルまたはローカルに再定義することを、未定義の動作を呼び出すこととして分類しています。したがって、このような使用法は、実装によってエラーであると診断できます。したがって、Lisp パッケージ システムは、名前の衝突の例と見なすことができるマクロ衛生問題に対する実行可能で完全なソリューションを提供します。
たとえば、プログラム定義関数の再定義の例では、my-unlessマクロは独自のパッケージに存在でき、 はuser-defined-operatorそのパッケージ内のプライベート シンボルです。ユーザー コードで出現するシンボルは、マクロuser-defined-operatorの定義で使用されているシンボルとは無関係の別のシンボルになりますmy-unless。
リテラルオブジェクト
一部の言語では、マクロの展開はテキスト コードに対応している必要はなく、シンボル を含む式に展開するのではなくf、マクロは によって参照される実際のオブジェクトを含む展開を生成する場合がありますf。同様に、マクロがマクロのパッケージで定義されているローカル変数またはオブジェクトを使用する必要がある場合は、マクロ定義の環境を囲む語彙環境を持つクロージャ オブジェクトの呼び出しに展開できます。
衛生的な変革
Schemeなどの言語の衛生的なマクロ システムは、すべての識別子の語彙スコープを保持し、偶発的なキャプチャを防止するマクロ拡張プロセスを使用します。このプロパティは参照透過性と呼ばれます。キャプチャが必要な場合、一部のシステムでは、プログラマがマクロ システムの衛生メカニズムを明示的に違反することを許可しています。
たとえば、Scheme のlet-syntaxマクロdefine-syntax作成システムは衛生的であるため、次の Scheme 実装ではmy-unless目的の動作が実現されます。
(定義構文my-unless (構文ルール() (( _条件本体... ) ( if ( not条件) (本体の開始... )))))
( let (( not ( lambda ( x ) x ))) ( my-unless #t ( display "これは印刷されるべきではありません!" ) (改行)))
入力フォームのパターンを出力フォームに変換する役割を持つ衛生的なマクロ プロセッサは、シンボルの衝突を検出し、シンボルの名前を一時的に変更することで衝突を解決します。基本的な戦略は、マクロ定義内のバインディングを識別してそれらの名前を gensyms に置き換え、マクロ定義内の自由変数を識別してそれらの名前がマクロが使用されたスコープではなくマクロ定義のスコープ内で検索されるようにすることです。
実装
自動的に衛生状態を強化するマクロ システムは、Scheme に端を発しています。衛生的なマクロ システムの最初の KFFD アルゴリズムは、Kohlbecker によって 1986 年に発表されました。[3]当時、Scheme の実装では標準的なマクロ システムは採用されていませんでした。その後まもなく、1987 年に Kohlbecker とWandは、マクロを記述するための宣言的なパターン ベース言語を提案しましたsyntax-rules。これは、R5RS 標準で採用されたマクロ機能の前身でした。 [1] [7]代替の衛生メカニズムである構文クロージャは、1988 年に Bawden と Rees によって、Kohlbecker らのシステムの代替として提案されました。[8] KFFD アルゴリズムとは異なり、構文クロージャでは、プログラマが識別子のスコープの解決を明示的に指定する必要があります。1993 年に Dybvig らは、syntax-case構文の代替表現を使用して衛生状態を自動的に維持するマクロ システムを導入しました。[9]このシステムでは、パターン言語を派生マクロとしてsyntax-case表現できます。マクロ システムsyntax-rulesという用語は、Scheme のコンテキストでは、パターン マッチング構造 (例: 構文ルール) と構文を表現および操作するためのフレームワーク (例: 構文ケース、構文クロージャ) の両方を指す可能性があるため、曖昧になる可能性があります。
構文規則
構文ルールは、マクロの記述を容易にする高レベルのパターン マッチングsyntax-rules機能です。ただし、マクロの特定のクラスを簡潔に記述することはできず、他のマクロ システムを表現するには不十分です。構文ルールは R4RS ドキュメントの付録で説明されていましたが、必須ではありませんでした。その後、R5RS はこれを標準マクロ機能として採用しました。2syntax-rulesつの変数の値を交換するマクロの例を次に示します。
(構文を定義してswap! (構文ルール() (( _ a b ) ( let (( temp a )) ( set! a b ) ( set! b temp )))))
構文ケース
syntax-rules純粋にマクロベースのシステムには欠陥があるため、 R6RS Scheme 標準では構文ケース マクロ システムを採用しました。[10]とは異なりsyntax-rules、syntax-caseにはパターン マッチング言語とマクロを記述するための低レベルの機能の両方が含まれています。前者はマクロを宣言的に記述することを可能にし、後者はマクロを記述するための代替フロントエンドの実装を可能にします。前の swap の例は、syntax-caseパターン マッチング言語が似ているため、とほぼ同じです。
(定義構文swap! (ラムダ( stx ) (構文ケースstx () (( _ a b ) (構文( let (( temp a )) ( set! a b ) ( set! b temp )))))))
ただし、syntax-caseは syntax-rules よりも強力です。たとえば、syntax-caseマクロは任意の Scheme 関数を介してパターン マッチング ルールに副条件を指定できます。あるいは、マクロ作成者はパターン マッチング フロントエンドを使用せず、構文を直接操作することもできます。 関数を使用するとdatum->syntax、 syntax-case マクロは意図的に識別子をキャプチャして、衛生状態を崩すこともできます。
その他のシステム
Scheme では他のマクロ システムも提案され、実装されています。構文クロージャと明示的な名前変更[11]は、2 つの代替マクロ システムです。どちらのシステムも syntax-rules よりも低レベルで、マクロ作成者に衛生管理の強制を委ねています。これは、デフォルトで自動的に衛生管理を強制する syntax-rules と syntax-case とは異なります。上記の swap の例を、それぞれ構文クロージャと明示的な名前変更の実装を使用して次に示します。
;; 構文クロージャ
( define-syntax swap! ( sc-macro-transformer ( lambda ( form environment ) ( let (( a ( close-syntax ( cadr form ) environment )) ( b ( close-syntax ( caddr form ) environment ))) ` ( let (( temp , a )) ( set! , a , b ) ( set! , b temp ))))))
;; 明示的な名前変更
( define-syntax swap! ( er-macro-transformer ( lambda ( form rename compare ) ( let (( a ( caddr form )) ( b ( caddr form )) ( temp ( rename 'temp ))) ` ( , ( rename 'let ) (( , temp , a )) ( , ( rename 'set! ) , a , b ) ( , ( rename 'set! ) , b , temp ))))))
衛生的なマクロシステムを備えた言語
- Scheme – 構文ルール、構文ケース、構文クロージャなど。
- Racket – Scheme のバリエーションで、そのマクロ システムは元々 syntax-case に基づいていましたが、現在はより多くの機能を備えています。
- ネメルレ[12]
- ディラン
- エリクサー[13]
- ニム
- さび
- ハクセ
- Mary2 – 1978 年頃のALGOL 68派生言語のスコープ付きマクロ本体
- ジュリア[14]
- Raku – 衛生的なマクロと非衛生的なマクロの両方をサポート[15]
批判
衛生的なマクロは、意図的な変数の捕捉を難しくする代わりに、安全性と参照の透明性を提供します。Let Over Lambdaの著者であるDoug Hoyteは次のように書いています。[16]
変数キャプチャの影響を減らすために取られるほとんどすべてのアプローチは、defmacro で実行できることを減らすことしかできません。衛生的なマクロは、最良の状況では初心者の安全ガードレールになりますが、最悪の状況では電気柵を形成し、犠牲者を衛生的でキャプチャが安全な刑務所に閉じ込めます。
— ダグ・ホイット
多くの衛生的なマクロシステムは、衛生が提供する保証を損なうことなく、逃げ道を提供しています。たとえば、Racket では構文パラメータを定義できるため、バインドされた変数を選択的に導入できます。Gregg Hendershott は、Fear of Macros [17]で、この方法でアナフォリック if 演算子を実装する例を示しています。
参照
注記
- ^ ab リチャード・ケルシー、ウィリアム・クリンガー、ジョナサン・リース他 (1998年8月)。「アルゴリズム言語スキームに関する改訂報告書」高階および記号計算。11 (1): 7–105。doi : 10.1023 /A : 1010051815785。
- ^ Feinberg, N.; Keene, SE; Matthews, RO; Withington, PT (1997)、「Dylan プログラミング: オブジェクト指向の動的言語」、Addison Wesley Longman Publishing Co., Inc.
- ^ ab Kohlbecker, E.; Friedman, DP; Felleisen, M.; Duba, B. (1986). 「Hygienic Macro Expansion」(PDF) . ACM conference on LISP and functional programming .
- ^ 「CLHS: 関数 GENSYM」。
- ^ “hygiene-versus-gensym”. community.schemewiki.org . 2022年6月11日閲覧。
- ^ Costanza, Pascal; D'Hondt, Theo (2010). 「非衛生的なマクロシステムへの衛生的互換マクロの埋め込み」. Journal of Universal Computer Science . 16 (2): 271–295. CiteSeerX 10.1.1.424.5218 . doi : 10.3217/jucs-016-02-0271 .
- ^ Kohlbecker, E.; Wand, M. (1987). 「例によるマクロ: 仕様からの構文変換の導出」(PDF) .プログラミング言語の原理に関するシンポジウム.
- ^ Bawden, A.; Rees, J. (1988). 「構文クロージャ」(PDF) . Lisp and Functional Programming . 2019年9月3日時点のオリジナルよりアーカイブ(PDF) 。
- ^ Dybvig, K; Hieb, R; Bruggerman, C (1993). 「Scheme における構文抽象化」(PDF) . LISP と記号計算. 5 (4): 295–326. doi :10.1007/BF01806308. S2CID 15737919.
- ^ Sperber, Michael; Dybvig, R. Kent; Flatt, Matthew; Van Straaten, Anton; 他 (2007 年 8 月)。「Revised6 Report on the Algorithmic Language Scheme (R6RS)」。Scheme 運営委員会。2011年 9 月 13 日閲覧。
- ^ Clinger, Will (1991). 「明示的な名前変更による衛生的なマクロ」ACM SIGPLAN Lisp ポインタ4 ( 4): 25–28. doi :10.1145/1317265.1317269. S2CID 14628409.
- ^ Skalski, K.; Moskal, M; Olszta, P, Metaprogramming in Nemerle (PDF) 、 2012-11-13 のオリジナル(PDF)からアーカイブ
- ^ 「マクロ」。
- ^ 「メタプログラミング: Julia 言語」。2013 年 5 月 4 日時点のオリジナルよりアーカイブ。2014年 3 月 3 日閲覧。
- ^ 「概要 6: サブルーチン」。2014 年 1 月 6 日時点のオリジナルよりアーカイブ。2014年 6 月 3 日閲覧。
- ^ [1]、ラムダを超えて—Lispの50年、Doug Hoyte著
- ^ [2]、マクロに対する恐怖
