17

PeterSeibelの著書「PracticalCommonLisp」では、非常に複雑なマクロの定義を1回だけ見つけることができます(ページの下部http://www.gigamonkeys.com/book/macros-defining-your-own.htmlを参照)。 )。

このマクロ定義を過去3週間で10回読んでいますが、どのように機能するのか理解できません。:(さらに悪いことに、その目的と使用方法を理解していても、このマクロを自分で開発することはできません。

私は特に、この悪名高いハードマクロの体系的な「導出」に段階的に興味を持っています。何か助けはありますか?

4

3 に答える 3

25

あなたはこれを見ていますか:

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

それほど複雑ではありませんが、ネストされた逆引用符と、互いに類似した複数のレベルがあり、経験豊富な Lisp コーダーでさえ混乱を招きやすいものです。

これは、展開を記述するためにマクロによって使用されるマクロです: マクロの本体の一部を書き込むマクロです。

マクロ自体の本体にはプレーンがありlet、その後、一度バッククォートされて生成さletれ、 を使用するマクロの本体内に存在しますonce-only。最後に、マクロがユーザーによって使用されるコード サイトで、そのletマクロのマクロ展開に表示される二重バッククォートがあります。

gensyms を生成する 2 回のラウンドが必要なのonce-onlyは、それ自体がマクロであり、それ自体のために衛生的でなければならないからです。そのため、最も外側に自分自身のgensymsの束を生成しletます。また、 の目的はonce-only、別の衛生的なマクロの記述を簡素化することです。そのため、そのマクロの gensyms も生成されます。

一言で言えば、once-only値が gensyms であるいくつかのローカル変数を必要とするマクロ展開を作成する必要があります。これらのローカル変数は、gensym を別のマクロ展開に挿入して衛生的にするために使用されます。そして、これらのローカル変数は、マクロ展開であるため、それ自体が衛生的でなければならないため、gensyms でもあります。

単純なマクロを書いている場合、gensyms を保持するローカル変数があります。

;; silly example
(defmacro repeat-times (count-form &body forms)
  (let ((counter-sym (gensym)))
    `(loop for ,counter-sym below ,count-form do ,@forms)))

マクロを作成する過程で、シンボルcounter-sym. この変数はプレーン ビューで定義されます。人間であるあなたは、語彙の範囲内で何かと衝突しないようにそれを選択しました。問題のレキシカル スコープは、マクロのスコープです。counter-sym内部の参照を誤ってキャプチャすることを心配する必要はありません。count-formまたは、一部のリモート レキシカル スコープ (マクロが使用されるサイト) に挿入されるコードの一部に入る単なるデータであるformsためです。マクロ内の別の変数とforms混同しないように注意する必要があります。counter-symたとえば、ローカル変数に名前を付けることはできませんcount-form。なんで?その名前は関数の引数の 1 つだからです。それを隠して、プログラミング エラーを作成します。

マクロを使ってそのマクロを作成したい場合は、マシンがユーザーと同じ仕事をしなければなりません。コードを書いているときは、変数名を考案する必要があり、考案した名前に注意する必要があります。

ただし、コード作成マシンは、あなたとは異なり、周囲のスコープを認識しません。そこにある変数を単純に見て、衝突しない変数を選択することはできません。マシンは、いくつかの引数 (評価されていないコードの断片) を取り、そのマシンがその仕事を終えた後に盲目的にスコープに代入されるコードの断片を生成する単なる関数です。

したがって、マシンはより賢明に名前を選択する必要があります。実際、完全に防弾であるためには、偏執的であり、完全にユニークなシンボルであるgensymsを使用する必要があります.

例を続けて、このマクロ本体を作成するロボットがあるとします。そのロボットはマクロにすることができますrepeat-times-writing-robot:

(defmacro repeat-times (count-form &body forms)
  (repeat-times-writing-robot count-form forms))  ;; macro call

ロボット マクロはどのように見えるでしょうか?

(defmacro repeat-times-writing-robot (count-form forms)
  (let ((counter-sym-sym (gensym)))     ;; robot's gensym
    `(let ((,counter-sym-sym (gensym))) ;; the ultimate gensym for the loop
      `(loop for ,,counter-sym-sym below ,,count-form do ,@,forms))))

これが の特徴のいくつかをどのように持っているかを見ることができますonce-only: 二重の入れ子と の 2 つのレベル(gensym)。これが理解できれば、飛躍once-onlyは小さいです。

もちろん、ロボットに繰り返し回数を書かせたいだけなら、それを関数にすれば、その関数は変数の発明について心配する必要がなくなります: それはマクロではないので、衛生は必要ありません:

 ;; i.e. regular code refactoring: a piece of code is moved into a helper function
 (defun repeat-times-writing-robot (count-form forms)
   (let ((counter-sym (gensym)))
     `(loop for ,counter-sym below ,count-form do ,@forms)))

 ;; ... and then called:
(defmacro repeat-times (count-form &body forms)
  (repeat-times-writing-robot count-form forms))  ;; just a function now

しかし、その仕事はボス、それを使用するマクロに代わって変数を発明することであり、関数は呼び出し元に変数を導入できないため、関数にすることはできませんonce-only

于 2012-03-21T18:28:05.910 に答える
7

Practical Common Lispのマクロに代わる ものがLet Over Lambdaonce-onlyで派生しています (第 3 章の「1 回のみ」セクションを参照)。

于 2012-03-21T19:15:17.120 に答える
1

カズはそれを美しく広範囲に説明しました。

ただし、二重衛生の問題をあまり気にしない場合は、次のほうが理解しやすいかもしれません。

(defmacro once-only ((&rest symbols) &body body)
  ;; copy-symbol may reuse the original symbol name
  (let ((uninterned-symbols (mapcar 'copy-symbol symbols)))
    ;; For the final macro expansion:
    ;; Evaluate the forms in the original bound symbols into fresh bindings
    ``(let (,,@(mapcar #'(lambda (uninterned-symbol symbol)
                           ``(,',uninterned-symbol ,,symbol))
                       uninterned-symbols symbols))
        ;; For the macro that is using us:
        ;; Bind the original symbols to the fresh symbols
        ,(let (,@(mapcar #'(lambda (symbol uninterned-symbol)
                             `(,symbol ',uninterned-symbol))
                         symbols uninterned-symbols))
           ,@body))))

最初のletものは、最終展開の一部になるため、2 回バッククォートされます。目的は、元のバインドされたシンボルのフォームを新しいバインドに評価することです。

2 番目letは のユーザーの一部になるため、1 回バッククォートされますonce-only。目的は、元のシンボルを新しいシンボルに再バインドすることです。これは、それらのフォームが評価され、最終的な展開でそれらにバインドされるためです。

元のシンボルの再バインドが最終的なマクロ展開の前に行われた場合、最終的なマクロ展開は元の形式ではなく、インターンされていないシンボルを参照します。

with-slotsその使用の実装はonce-only、二重の衛生状態を必要とする例です。

(defmacro with-slots ((&rest slots) obj &body body)
  (once-only (obj)
    `(symbol-macrolet (,@(mapcar #'(lambda (slot)
                                     `(,slot (slot-value ,obj ',slot)))
                                 slots))
       ,@body)))

;;; Interaction in a REPL    
> (let ((*gensym-counter* 1)
        (*print-circle* t)
        (*print-level* 10))
    (pprint (macroexpand `(with-slots (a) (make-object-1)
                            ,(macroexpand `(with-slots (b) (make-object-2)
                                             body))))))

;;; With the double-hygienic once-only
(let ((#1=#:g2 (make-object-1)))
  (symbol-macrolet ((a (slot-value #1# 'a)))
    (let ((#2=#:g1 (make-object-2)))
      (symbol-macrolet ((b (slot-value #2# 'b)))
        body))))

;;; With this version of once-only
(let ((#1=#:obj (make-object-1)))
  (symbol-macrolet ((a (slot-value #1# 'a)))
    (let ((#1# (make-object-2)))
      (symbol-macrolet ((b (slot-value #1# 'b)))
        body))))

2 番目の展開は、innerがoutterletの変数へのバインディングをシャドーイングしていることを示しています。したがって、インナー内でアクセスすると、実際には 2 番目のオブジェクトにアクセスします。#:objletawith-slots

この例では、外側のマクロ展開が という名前の gensymg2と内側の を取得することに注意してくださいg1。通常の評価またはコンパイルでは、フォームは外側から内側に移動するため、逆になります。

于 2012-04-15T15:13:37.423 に答える