うまくいけば、これは冗長な質問ではありません。
スキームの新参者として、不要な複雑さを犠牲にして、syntax-case
マクロが代替手段よりも強力であることを認識しています。syntax-rules
syntax-rules
しかし、Common Lisp のマクロ システムをスキームで実装することは可能syntax-case
ですか?
うまくいけば、これは冗長な質問ではありません。
スキームの新参者として、不要な複雑さを犠牲にして、syntax-case
マクロが代替手段よりも強力であることを認識しています。syntax-rules
syntax-rules
しかし、Common Lisp のマクロ システムをスキームで実装することは可能syntax-case
ですか?
簡潔にしようと思いますが、これは一般的に非常に深い問題であり、Q&A の平均的な SO レベルよりも難しいため、これはかなり長くなります。また、偏見を持たないように努めます。私は Racket の観点から来ましたが、過去に Common Lisp を使用しており、両方の世界で (実際には他の世界でも) 常にマクロを使用するのが好きでした。それはあなたの質問に対する直接的な答えのようには見えませんが (極端な場合、それは単に「はい」になります)、2 つのシステムを比較しているので、これが人々、特に外部の人々にとって問題を明確にするのに役立つことを願っています。なぜこれがそんなに大したことなのか不思議に思う Lisp (すべてのフレーバー) の。やり方も書いておきますdefmacro
「構文ケース」システムで実装できますが、一般的には物事を明確にするためだけです(そのような実装を見つけることができるので、コメントや他の回答でいくつか与えられています)。
まず、あなたの質問は非常に冗長ではありません - それは非常に正当化されており、(私が暗示したように) Lisp から Scheme への初心者、Scheme から Lisp への初心者が遭遇するものの 1 つです。
第二に、非常に浅い、非常に短い答えは、人々があなたに言ったことです: はい、CLdefmacro
をサポートするスキームに実装することは可能syntax-case
です。これとは逆に、syntax-case
simple を使用して実装するdefmacro
ことは、私があまり話しませんが、よりトリッキーなテーマです。再実装やその他のバインディング構造に非常に高いコストがかかるだけで、それは完了したとだけ言っておきます。lambda
つまり、基本的には、その実装を使用したい場合にコミットする必要がある新しい言語の再実装です。 .
もう 1 つの明確化: 人々、特に CLer は、Scheme マクロに関連する 2 つのこと (衛生とsyntax-rules
. 問題は、R5RSsyntax-rules
にあるのは、非常に限定されたパターンベースの書き換えシステムである だけです。他の書き換えシステムと同様に、単純に使用することも、書き換えを使用して小さな言語を定義し、それを使用してマクロを記述することもできます。このテキストを参照してください。これがどのように行われるかについての既知の説明については。それを行うことは可能ですが、肝心なのは、実際の言語に直接関係のない奇妙な小さな言語を使用しているため、それは難しいということです。これにより、Scheme プログラミングとはかけ離れたものになります。おそらくさらに悪いことに、 CL で衛生的なマクロ実装を使用することは、実際にはプレーンな CL を使用していないということです。つまり、 のみを使用できますsyntax-rules
が、これは主に理論的な意味であり、「実際の」コードで使用したいものではありません。ここでの主なポイントは、衛生とは に限定されることを意味するものではないということsyntax-rules
です。
ただし、 「その」Schemeマクロシステムとして意図されたわけではありません-実装に使用されるが、衛生破りのマクロも実装できるsyntax-rules
「低レベル」マクロ実装があるという考えが常にありました-それはありませんでしたsyntax-rules
特定の低レベルの実装に関する合意。R6RS は、「syntax-case」マクロ システムを標準化することでこれを修正します (システムの名前として「syntax-case」を使用していることに注意してください。これは、主なハイライトである形式とは異なりsyntax-case
ます)。議論がまだ生きていることを強調するかのように、R7RSは一歩下がってそれを除外し、syntax-rules
少なくとも「小さな言語」に関する限り、低レベルのシステムに関与しないことに戻りました.
さて、実際に 2 つのシステムの違いを理解するには、それらが扱うタイプの違いを明確にするのが最善です。ではdefmacro
、トランスフォーマーは基本的に S 式を受け取り、S 式を返す関数です。ここでの S 式は、一連のリテラル型 (数値、文字列、ブール値)、シンボル、およびこれらのリスト ネストされた構造で構成される型です。(実際に使用される型はそれよりも少し多いですが、要点を説明するにはそれで十分です。) 問題は、これが非常に単純な世界であるということです。非常に具体的なものに到達しているため、実際に入力を出力できます。 /output の値だけです。このシステムは記号を使用していることに注意してください識別子を表す - シンボルはまさにその意味で非常に具体的なものです: anx
はまさにその名前を持つコードの一部ですx
.
ただし、この単純さには代償が伴います。両方ともx
. 通常の CL ベースにdefmacro
は、これを補う追加のビットがいくつかあります。そのようなビットの 1 つgensym
は、インターンされていない「新鮮な」シンボルを作成するためのツールです。したがって、同じ名前を持つシンボルを含め、他のシンボルとは異なることが保証されます。もう 1 つのそのようなビットは、トランスフォーマー&environment
への引数defmacro
で、マクロが使用される場所の字句環境の表現を保持します。
defmacro
プレーンな印刷可能な値をもはや扱っていないため、また、環境の何らかの表現を認識する必要があるため、これらのことが世界を複雑にしていることは明らかです。これにより、マクロが実際にはコンパイラー・フック (この環境は基本的に、コンパイラーが通常扱うデータ型であり、単なる S 式よりも複雑なものであるため)。しかし、結局のところ、それらは衛生を実装するのに十分ではありません. 使用するgensym
衛生管理の 1 つの簡単な側面 (マクロ側でのユーザー コードのキャプチャーの回避) を無効にすることはできますが、もう 1 つの側面 (ユーザー コードによるマクロ コードのキャプチャーの回避) は未解決のままです。一部の人々は、回避できる種類のキャプチャで十分であると主張して、これで解決します。はるかに重要です。
構文ケースのマクロ システムに切り替えます (そしてsyntax-rules
、 を使用して自明に実装されている を喜んでスキップしsyntax-case
ます)。このシステムでは、単純なシンボリック S 式が完全な語彙知識 (つまり、両方とも と呼ばれる 2 つの異なるバインディングの違いx
) を表すのに十分な表現力を持たない場合、それらを「強化」して使用するという考えがあります。するデータ型。(明示的な名前変更や構文上のクロージャなど、追加情報を提供するために異なるアプローチを取る他の低レベル マクロ システムがあることに注意してください。)
これを行う方法は、マクロ トランスフォーマーを、まさにそのような表現である「構文オブジェクト」を消費して返す関数にすることです。より正確には、これらの構文オブジェクトは通常、単純なシンボリック表現の上に構築され、レキシカル スコープを表す追加情報を持つ構造にラップされているだけです。一部のシステム (特に Racket) では、すべてが構文オブジェクト (シンボルやその他のリテラルやリスト) にラップされます。これを考えると、構文オブジェクトから S 式を簡単に取得できることは驚くべきことではありません。シンボリックな内容を取り出すだけで、それがリストの場合は再帰的にそれを続けます。構文ケースのシステムでは、これはsyntax-e
、構文オブジェクトのシンボリック コンテンツのアクセサーを実装することによって行われます。syntax->datum
これは、完全な S 式を生成するために結果を再帰的に下げるバージョンを実装します。補足として、これは、Scheme でバインディングがシンボルとして表現されず、識別子として表現されることについて話す理由の大まかな説明です。
一方、問題は、与えられた記号名からどのように開始し、そのような構文オブジェクトを構築するかです。これはdatum->syntax
関数を使用して行われますが、レキシカル スコープ情報がどのように表現されるかを API に指定させる代わりに、関数は構文オブジェクトを最初の引数として、シンボリック S 式を 2 番目の引数として受け取ります。最初から取得した字句スコープ情報で S 式を適切にラップすることにより、構文オブジェクトを作成します。つまり、衛生状態を破るには、通常、ユーザー提供の構文オブジェクト (マクロの本体形式など) から開始し、その字句情報を使用しthis
て、同じスコープで表示されるような新しい識別子を作成します。
この簡単な説明は、表示されたマクロがどのように機能するかを理解するのに十分です。@ChrisJester-Young が示したマクロは、単純に構文オブジェクトを取り込み、それを で生の S 式にストリップし、それをトランスフォーマーsyntax->datum
に送信してS 式を取得し、結果を次のように変換するために使用します。ユーザーコードの字句コンテキストを使用する構文オブジェクト。ラケットのdefmacro
syntax->datum
defmacro
実装は少し手の込んだものです。ストリッピング段階では、結果の S 式を元の構文オブジェクトにマップするハッシュ テーブルを保持し、再構築段階では、このテーブルを参照して、コードのビットが最初に持っていたのと同じコンテキストを取得します。これにより、いくつかのより複雑なマクロに対してより堅牢な実装になりますが、構文オブジェクトにはソースの場所、プロパティなど、より多くの情報が含まれているため、Racket でもより便利です。この慎重な再構築により、通常、出力値が得られます (構文オブジェクト) は、マクロへの途中で持っていた情報を保持します。
defmacro
構文ケース システムのプログラマ向けのもう少し技術的な紹介については、マクロの記述syntax-case
に関するブログ投稿を参照してください。あなたがScheme側から来ているなら、それはそれほど役に立ちませんが、それでも問題全体を明確にするのに役立ちます.
これを結論に近づけるために、非衛生的なマクロを扱うことは依然として難しい場合があることに注意する必要があります。より具体的には、そのような結合を達成するためのさまざまな方法がありますが、それらはさまざまな微妙な方法で異なり、通常、戻ってきて、それぞれの場合にわずかに異なる歯の跡を残して噛むことがあります. CL のような「真の」defmacro
システムでは、比較的よく知られている特定の歯のマークのセットと一緒に暮らすことを学びます。最も注目すべきは、Racket が頻繁に使用する同じ名前に対して異なるバインディングを持つ言語のモジュール構成の一種です。構文ケースのシステムでは、より良いアプローチはfluid-let-syntax
これは、字句スコープの名前の意味を「調整」するために使用されます。最近では、「構文パラメーター」に進化しています。hygienicのみ、基本的な構文ケース、CL-style 、そして最後に構文パラメーターを使用して問題を解決する方法の説明が含まれています。このテキストは少し専門的になりますが、最初の数ページは比較的読みやすく、これを理解すれば、議論全体を非常によく理解できます。(論文の方が適切にカバーされている古いブログ投稿もあります。)syntax-rules
defmacro
これは、マクロに関する唯一の「ホットな」問題というわけではありません。どちらの低レベル マクロ システムが優れているかについての Scheme サークル内での議論は、非常に熱くなることがあります。また、ライブラリが値と関数だけでなくマクロを提供できるモジュール システムでマクロを機能させる方法や、マクロ展開時間とランタイムを別々のフェーズに分割するかどうかなど、マクロに関する他の問題もあります。
これにより、トレードオフを認識し、自分に最適なものを自分で決定できるようになるまで、問題のより完全な全体像が示されることを願っています. また、これが通常の炎の原因のいくつかを明らかにすることを願っています: 衛生的なマクロは確かに役に立たないわけではありませんが、新しい型は単純な S 式以上のものであるため、それらの周りにはより多くの機能があります - そして、あまりにも多くの場合、浅はかです -バイパスを読んでいる人は、「複雑すぎる」という結論に飛びつきます。さらに悪いのは、「Scheme の世界では、人々はメタプログラミングについてほとんど何も知らない」という精神の炎です: 追加のコストと望ましい利益を非常に痛感しているため、Scheme の世界の人々は桁違いの集団的努力を費やしてきました。件名に。固執するのは良い選択ですdefmacro
S 式の余分なラッパーがあなたの好みには複雑すぎる場合は、それを学習するためのコストと、衛生を捨てて支払うもの (およびそれを受け入れることで得られるもの) に注意する必要があります。
残念なことに、あらゆる種類のマクロは初心者にとって全体的にかなり難しいテーマです (おそらく非常に限られたものを除くsyntax-rules
)。最終的に、トレードオフを明確にするために、両方の世界で良い経験を積むことに勝るものはありません。(そして、それは非常に具体的な個人的な経験からのものです: PLT スキームが N 年前に構文ケースに切り替えられていなかったら、おそらく気にすることはなかったでしょう...一度切り替えると、コードを変換するのに長い時間がかかりました-そしてそのとき初めて、名前が誤って「混乱」することのない堅牢なシステムを持つことがいかに優れているかを実感しました (これにより、奇妙なバグが発生したり、難読化されたりする可能性があります%%__names__
)。
(それでも、コメント炎上する可能性は高いですが…)
これが Guile の の実装ですdefine-macro
。で完全に実装されていることに注意してくださいsyntax-case
。
(define-syntax define-macro
(lambda (x)
"Define a defmacro."
(syntax-case x ()
((_ (macro . args) doc body1 body ...)
(string? (syntax->datum #'doc))
#'(define-macro macro doc (lambda args body1 body ...)))
((_ (macro . args) body ...)
#'(define-macro macro #f (lambda args body ...)))
((_ macro transformer)
#'(define-macro macro #f transformer))
((_ macro doc transformer)
(or (string? (syntax->datum #'doc))
(not (syntax->datum #'doc)))
#'(define-syntax macro
(lambda (y)
doc
#((macro-type . defmacro)
(defmacro-args args))
(syntax-case y ()
((_ . args)
(let ((v (syntax->datum #'args)))
(datum->syntax y (apply transformer v)))))))))))
Guile は Common Lisp スタイルの docstring を特別にサポートしているため、Scheme の実装で docstring を使用しない場合、define-macro
実装はさらに簡単になります。
(define-syntax define-macro
(lambda (x)
(syntax-case x ()
((_ (macro . args) body ...)
#'(define-macro macro (lambda args body ...)))
((_ macro transformer)
#'(define-syntax macro
(lambda (y)
(syntax-case y ()
((_ . args)
(let ((v (syntax->datum #'args)))
(datum->syntax y (apply transformer v)))))))))))