20

両方の言語についてもっと学ぶ方法として、 Haskell( GitHubのコード)でLispを書いています。

私が追加している最新の機能はマクロです。衛生的なマクロや派手なものではなく、単なるバニラコード変換です。私の最初の実装には、他のすべての値が存在する環境とは異なる、別個のマクロ環境がありました。readと関数の間に、コードツリーをウォークし、マクロ環境でキーワードが見つかるたびに適切な変換を実行するeval別の関数を散在させました。最終的なフォームが評価macroExpandのために渡される前。evalこれの優れた利点は、マクロが他の関数と同じ内部表現を持ち、コードの重複を減らすことでした。

ただし、2つの環境があると不格好に見え、ファイルをロードするeval場合、ファイルにマクロ定義が含まれている場合に備えて、マクロ環境にアクセスする必要があることに悩まされました。そこで、マクロタイプを導入し、関数や変数と同じ環境にマクロを格納し、マクロ展開フェーズをに組み込むことにしましたeval。私は最初、このコードを書くことができると思うまで、それを行う方法に少し戸惑いました。

eval env (List (function : args)) = do
    func <- eval env function
    case func of 
        (Macro {}) -> apply func args >>= eval env
        _          -> mapM (eval env) args >>= apply func

次のように機能します。

  1. 最初の式と他の式の束を含むリストが渡された場合...
  2. 最初の式を評価する
  3. マクロの場合は、引数に適用して結果を評価します
  4. マクロでない場合は、引数を評価し、結果に関数を適用します

eval / applyの順序が入れ替わっていることを除けば、マクロは関数とまったく同じです。

これはマクロの正確な説明ですか?このようにマクロを実装することで、何か重要なことが欠けていますか?答えが「はい」と「いいえ」の場合、なぜこれまでこのように説明されたマクロを見たことがないのでしょうか。

4

6 に答える 6

21

答えは「いいえ」と「はい」です。

マクロ レベルとランタイム レベルが別々の世界にあるマクロの優れたモデルから始めたようです。実際、これはラケットマクロシステムの背後にある主要なポイントの 1 つです。これについては、ラケット ガイドで簡単なテキストを読むか、この機能について説明している元の論文と、それを実行することをお勧めする理由を参照してください。Racket のマクロ システムは非常に洗練されたものであり、衛生的であることに注意してください。ただし、相分離は、衛生面に関係なく良い考えです。主な利点をまとめると、コードを常に確実に展開できるため、個別コンパイルなどの利点が得られ、コードのロード順序に依存しないなどの問題があります。

次に、単一の環境に移動すると、それが失われます。ほとんどの Lisp の世界 (CL や Elisp など) では、まさにこのように処理が行われます。明らかに、上記の問題に遭遇します。(相分離はこれらを回避するように設計されているため、"明らか" であり、たまたま歴史的に起こったのとは逆の順序で発見を得ることができました。) いずれにせよ、これらの問題のいくつかに対処するために、eval-when特別な形式があります。一部のコードが実行時またはマクロ展開時に評価されるように指定します。Elispでは、次のように取得しますeval-when-compile、しかしCLでは、他のいくつかの「*-time」を使用して、はるかに多くの髪を取得します. (CL には読み取り時間もあり、他のすべてのものと同じ環境を共有することで、楽しみが 3 倍になります。) 良いアイデアのように思えますが、周りを読んで、この混乱のために一部のリスパーがどのように髪を失うかを確認する必要があります。

そして、説明の最後のステップで、さらに時間をさかのぼって、FEXPR として知られるものを発見します。私はそれについて何の指針も示しません、あなたはそれについてのたくさんのテキストを見つけることができます. 実際には、これら 2 つの「一部」はそれぞれ「ほとんど」と「少数」ですが、残っている FEXPR の拠点はわずかですが、声を上げることができます。これをすべて言い換えると、それは爆発的なものです... それについて質問することは、長いフレームウォーを得る良い方法です. (真剣な議論の最近の例として、R7RS の最初の議論期間を見ることができます。そこでは、FEXPR が登場し、まさにこの種の炎上につながりました。) どちらの側に座ることを選択したとしても、1 つのことは明らかです。FEXPR を含む言語は、FEXPR を含まない言語とは大きく異なります。[偶然にも、Haskell での実装に取り​​組むと、コードの正気の静的な世界に行く場所があるため、ビューに影響を与える可能性があります。そのため、「かわいい」超動的言語への誘惑はおそらくより大きくなります...]

最後の注意: あなたは似たようなことをしているので、Haskell でスキームを実装する同様のプロジェクトを調べる必要があります-- IIUC には、衛生的なマクロさえあります。

于 2012-04-20T16:07:36.810 に答える
16

そうではありません。実際、「名前による呼び出し」と「値による呼び出し」の違いをかなり簡潔に説明しました。値による呼び出し言語は、置換前に引数を値に減らします。名前による呼び出し言語は、最初に置換を実行し、次に削減を実行します。

主な違いは、マクロを使用すると参照透過性を破ることができることです。特に、マクロはコードを調べることができるため、通常のコードではできない方法で (3 + 4) と 7 を区別できます。そのため、マクロはより強力であると同時により危険でもあります。ほとんどのプログラマーは、(f 7) が 1 つの結果を生成し、(f (+ 3 4)) が別の結果を生成することに気付いた場合、動揺します。

于 2012-04-20T16:03:50.640 に答える
6

背景とりとめのない

あなたが持っているのは非常に遅いバインディングマクロです。これは実行可能なアプローチですが、同じコードを繰り返し実行するとマクロが繰り返し展開されるため、非効率的です。

良い面として、これはインタラクティブな開発に適しています。プログラマーがマクロを変更し、それを使用するコード(以前に定義された関数など)を再度呼び出すと、新しいマクロが即座に有効になります。これは直感的な「私が言っていることをする」行動です。

以前にマクロを展開するマクロシステムでは、プログラマーは、マクロが変更されたときにマクロに依存するすべての関数を再定義する必要があります。そうしないと、既存の定義は引き続き古いマクロ展開に基づいており、新しいバージョンのマクロには気づきません。 。

合理的なアプローチは、解釈されたコードにはこの遅延バインディングマクロシステムを使用することですが、コンパイルされたコードには「通常の」(より適切な単語がないため)マクロシステムを使用することです。

マクロを展開するために、別の環境は必要ありません。ローカルマクロは変数と同じ名前空間にある必要があるため、そうではありません。たとえば、Common Lispでこれを行う(let (x) (symbol-macrolet ((x 'foo)) ...))と、内側の記号マクロが外側の字句変数をシャドウイングします。マクロエクスパンダは、変数のバインド形式を認識している必要があります。およびその逆!let変数の内部がある場合x、それは外部をシャドウイングしsymbol-macroletます。xマクロエキスパンダーは、体内で発生するすべての発生を盲目的に置き換えることはできません。つまり、Lispマクロ拡張は、マクロと他の種類のバインディングが共存する完全な字句環境を認識している必要があります。もちろん、マクロ展開中に、同じ方法で環境をインスタンス化することはありません。もちろん、もしあれば(let ((x (function)) ..)(function)は呼び出されxず、値も指定されません。ただし、マクロエクスパンダはx、この環境にが存在することを認識しているため、の発生はxマクロではありません。

したがって、1つの環境と言うとき、私たちが実際に意味するのは、統合された環境には2つの異なる表現またはインスタンス化があるということです。拡張時間の表現と評価時間の表現です。実行時バインディングマクロは、これまでのように、これら2回を1つにマージすることで実装を簡素化しますが、そのようにする必要はありません。

Lispマクロはパラメータを受け入れることができることにも注意して&environmentください。macroexpandこれは、マクロがユーザーによって提供されたコードの一部を呼び出す必要がある場合に必要です。マクロを介してマクロエキスパンダーに戻るこのような再帰は、適切な環境を通過する必要があります。これにより、ユーザーのコードは、字句的に囲まれたマクロにアクセスし、適切に展開されます。

具体例

次のコードがあるとします。

(symbol-macrolet ((x (+ 2 2)))
   (print x)
   (let ((x 42)
         (y 19))
     (print x)
     (symbol-macrolet ((y (+ 3 3)))
       (print y))))

これがプリント4に与える影響42、、6。Common LispのCLISP実装を使用し、CLISPの実装固有の関数であるを使用してこれを拡張してみましょうsystem::expand-formmacroexpandローカルマクロに再帰されないため、通常の標準を使用することはできません。

(system::expand-form   
  '(symbol-macrolet ((x (+ 2 2)))
     (print x)
     (let ((x 42)
           (y 19))
       (print x)
       (symbol-macrolet ((y (+ 3 3)))
         (print y)))))

-->

(LOCALLY    ;; this code was reformatted by hand to fit your screen
  (PRINT (+ 2 2))
  (LET ((X 42) (Y 19))
    (PRINT X)
    (LOCALLY (PRINT (+ 3 3))))) ;

(まず、これらのlocallyフォームについて。なぜそこにあるのですか?symbol-macroletこれはおそらく宣言のためです。symbol-macroletフォームの本体に宣言がある場合は、その本体にスコープする必要があります。 、およびそれlocallyを実行します。の展開でsymbol-macroletこのラッピングが残らない場合、locally宣言のスコープは正しくありません。)

このマクロ展開から、タスクが何であるかを確認できます。マクロエクスパンダは、コードをウォークして、マクロシステムに関係するバインディングコンストラクトだけでなく、すべてのバインディングコンストラクト(実際にはすべての特殊な形式)を認識する必要があります。

のインスタンスの1つがその(print x)ままになっていることに注意してください:のスコープ内にあるもの(let ((x ..)) ...)。もう1つは(print (+ 2 2))、のシンボルマクロに従って、になりましたx

これから学ぶことができるもう1つのことは、マクロ展開は展開を置き換えてsymbol-macroletフォームを削除するだけであるということです。したがって、残っている環境は元の環境から、拡張プロセスでスクラブされたすべてのマクロマテリアルを除いたものになります。マクロ展開は、1つの大きな「大統一」環境ですべての字句バインディングを尊重(print (+ 2 2))しますが、その後、丁寧に気化して、のようなコードと他の痕跡だけを残し(locally ...)、非マクロバインディング構造だけが元の環境。

したがって、拡張されたコードが評価されると、縮小された環境の実行時のパーソナリティだけが機能します。letバインディングはインスタンス化され、初期値などが詰め込まれます。拡張中は、そのいずれも発生しませんでした。非マクロバインディングは、そのスコープを主張し、実行時の将来の存在を示唆するだけです。

于 2012-04-20T21:45:10.627 に答える
4

あなたが見逃しているのは、分析を評価から分離すると、この対称性が崩れることです。これは、すべての実用的な Lisp 実装が行うことです。マクロ展開は分析段階で発生するため、eval単純に保つことができます。

于 2012-04-20T15:40:59.113 に答える
2

Lisp の本を手元に置いておくことを強くお勧めします。推奨されるのは、たとえばChristian QueinnecLisp in Small Piecesです。この本はSchemeの実装に関するものです。

http://pagesperso-systeme.lip6.fr/Christian.Queinnec/WWW/LiSP.html

第 9 章はマクロに関するものです: http://pagesperso-systeme.lip6.fr/Christian.Queinnec/WWW/chap9.html

于 2012-04-20T18:15:31.650 に答える
1

その価値については、構文キーワードのScheme R 5 RSセクションBinding構文には、次のように書かれています。

Let-syntaxおよびはおよびletrec-syntaxに類似していますが、変数を値を含む場所にバインドするのではなく、構文キーワードをマクロトランスフォーマーにバインドします。letletrec

参照:http ://www.schemers.org/Documents/Standards/R5RS/HTML/r5rs-ZH-7.html#%_sec_4.3.1

これは、少なくともsyntax-rulesマクロシステムでは、別の戦略を使用する必要があることを意味しているようです。


マクロに別々の「場所」を使用するSchemeでいくつかの...興味深いコードを書くことができます。「実際の」コードに同じ名前のマクロと変数を混在させることはあまり意味がありませんが、試してみたい場合は、ChickenSchemeの次の例を検討してください。

#;1> let
Error: unbound variable: let
#;1> (define let +)
#;2> (let ((talk "hello!")) (write talk))
"hello!"
#;3> let
#<procedure C_plus>
#;4> (let 1 2)
Error: (let) not a proper list: (let 1 2)

    Call history:

    <syntax>                (let 1 2)       <--
#;4> (define a let)
#;5> (a 1 2)
3
于 2012-04-20T18:39:26.330 に答える