5

On Lisp では、p. 267 で、Paul Graham は、マクロを渡す継続の実装を提供します。

(setq *cont* #'identity)

(defmacro =lambda (parms &body body)
  `#'(lambda (*cont* ,@parms) ,@body))

(defmacro =defun (name parms &body body)
  (let ((f (intern (concatenate 'string
                "=" (symbol-name name)))))
    `(progn
       (defmacro ,name ,parms
     `(,',f *cont* ,,@parms))
       (defun ,f (*cont* ,@parms) ,@body))))

(defmacro =bind (parms expr &body body)
  `(let ((*cont* #'(lambda ,parms ,@body))) ,expr))

(defmacro =values (&rest retvals)
  `(funcall *cont* ,@retvals))

t2次のコードでは、 treeの各リーフのツリーをトラバースし、この実装を使用しています。特に、 のリーフが(最初の要素) から(2 番目の要素) に変更された後、 が呼び出されt1たときに何が起こるのか疑問に思っています。が呼び出されると、単純に からラムダ関数がポップされ、そのラムダ関数が再度呼び出されます。しかし、この呼び出しは最も外側の のスコープ外で行われ、バインディングを担当していました。では、アウターによって導入されたバインディングはどのようにスコープ内にあるのでしょうか?restartt1ABrestart*saved*dft-node(cdr tree)=bind=bind*cont**cont*=bind

(setq *saved* nil)

(=defun dft-node (tree)
    (cond ((null tree) (restart))
          ((atom tree) (=values tree))
          (t (push #'(lambda () (dft-node (cdr tree))) *saved*)
             (dft-node (car tree)))))

(=defun restart ()
    (if *saved*
        (funcall (pop *saved*))
      (=values 'done)))

(setq t1 '(a (b (d h)) (c e (f i) g))
      t2 '(1 (2 (3 6 7) 4 5)))

(=bind (node1) (dft-node t1)
  (if (eq node1 'done)
      'done
    (=bind (node2) (dft-node t2)
      (list node1 node2))))

最後の形式は次のように展開されます

(let ((*cont* (lambda (node1)
                (if (eq node1 'done)
                    'done
                    (let ((*cont* (lambda (node2)
                                    (list node1 node2))))
                      (dft-node t2))
  (dft-node t1))))))

これにより、 が生成され(a 1)ます。Graham によると、以降の への呼び出しはなどをまでrestart生成し、その後の呼び出しでは、 などを 最終的に まで生成する必要があります。(a 2)(a 5)(b 1)(b 2)(g 5)

> (let ((node1 (dft-node t1)))
    (if (eq? node1 ’done)
        ’done
        (list node1 (dft-node t2))))
(A 1)
> (restart)
(A 2)
…
> (restart)
(B 1)

の後、確立され(a 1)た のバインディングはもはや適切ではありません。これらの値への後続の呼び出しはどのように行われますか? のスコープは、への別の呼び出しにまだ適用されていますか? ここで何が起こっているのですか?*cont*letrestartletrestart

4

1 に答える 1

7

On Lispは Common Lisp より少し古いため、いくつかの非互換性があります

On Lispは、Common Lisp が実際に言語として固まる前に書かれたため、 On Lispと Common Lispに現れるコードの間にはいくつかの非互換性があります。On LispCLIki エントリは、これらの継続を渡すマクロが、実際には非互換性がある場所の 1 つであることを示しています (強調を追加)。

継続渡しマクロ (p. 267) を定義するとき、Paul Graham はcontグローバル変数にレキシカル スコープがあると想定しているようです。これは Common Lisp 標準に反します。現在の Common Lisp の実装では、前述のマクロは機能しません。また、この問題は初心者にとって非常に混乱する可能性があります。マクロを修正するための推奨される解決策は次のとおりです (Paul Graham の Errata によると、#'identity の代わりに #'values が使用されることに注意してください)。

  • let または lambda でシャドウイングできる symbol-macro を使用して、レキシカルスコープのグローバル変数cont
    をエミュレートします: (defvar actual-cont #'values)
    (define-symbol-macro *cont* *actual-cont*)
  • (setq *cont* #'identity) を省略して、「トップレベル」の継続渡し関数を (=somefunc #'values ...) として呼び出すだけです。
  • …</li>

これは問題の非常に短い説明であり、将来この問題に遭遇する初心者を助けるために、もう少し調べる価値があります。この同じ問題に関する次のような他の議論も参考になるかもしれません。

  • <On Lisp> の 268 ページ...ユーザーが(setq *cont* …)(defvar *cont* …)の違いについて尋ねる 2006 年からの comp.lang.lisp スレッド。このスレッドで、人々はOn Lispが ANSI Common Lisp よりも古いことに注目しています。

実際に何が起こっているのですか?

(=bind …)は(let ((*cont* …)) …)に展開されるため、 *cont*が特別な変数 (つまり、動的エクステントを持つ) である場合、その点で完全に正しいです。そのletの外側では、IDである*cont*の元のバインディングが、再起動の呼び出しのために配置されるべきものです。*cont*が特別に宣言されている場合、次の動作が得られます。

CONTINUATIONS> (=bind (node1) (dft-node '(a (b (d h)) (c e (f i) g)))
                 (if (eq node1 'done)
                     'done
                     (=bind (node2) (dft-node '(1 (2 (3 6 7) 4 5)))
                       (list node1 node2))))
(A 1)
CONTINUATIONS> (restart)
2
CONTINUATIONS> (restart)
3
CONTINUATIONS> (restart)
6
CONTINUATIONS> (restart)
7
CONTINUATIONS> (restart)
4
CONTINUATIONS> (restart)
5
CONTINUATIONS> (restart)
B
CONTINUATIONS> (restart)
D

(a 1) を取得した後、 *saved*には 2 つの関数が含まれているため、これは理にかなっています。1 つ目は、次のブランチで数値ツリーをトラバースし続けるもので、呼び出される*cont*はグローバルなもの#'identityです。これは、 =bindフォームの外にあるためです。そのため、結果として 2、3、… が得られます。この時点で*saved*にある 2 番目の関数は、B でアルファベット ツリーをたどり続ける関数です。

上記の (a 1)、(a 2)、…、(b 1) などのリストが得られない理由は、*cont*が特別である、つまり動的であると (合理的に) 仮定したためです。バウンド。Graham は*続き* が特別なものではないことを意図していることが判明しました。彼はそれをグローバルなレキシカルにしたいと考えています。On Lisp、268ページから:

操作*cont*することによって、継続の効果が得られます。はグローバルな値を持ってい*cont*ますが、これが使用されることはめったにありません。*cont*ほぼ常にパラメータであり、 によってキャプチャされ 、 によって=values定義されたマクロになります =defunadd1たとえば、の本体内には*cont*、グローバル変数ではなくパラメータがあります。これらのマクロは*cont*ローカル変数でなければ機能しないため、この区別は重要です。そのため *cont*、 の初期値は asetqではなくa で与えられますdefvar。後者は、それが特別であると宣言します。

On Lispは Common Lisp よりわずかに古いため、これを書いている時点では必ずしも正しくないわけではありませんが、Common Lisp には実際にはグローバルな字句変数がありませ。 level は必然的にグローバルなレキシカル変数を作成します。Common Lisp では、正確な結果は規定されていません。一部の実装では、これをグローバル レキシカルのように扱いますが、他の実装では、defparameterまたはdefvarを意味していると想定し、最終的にグローバルな特殊変数になります。Graham が指摘しているように、それはうまくいきません。後者を行う実装を持っているように聞こえるので、うまくいきません。

一部の実装では、実際に彼のコードについて不平を言うでしょう。たとえば、SBCL は、(setq *cont* …)「warning: undefined variable: CONTINUATIONS::*CONT*」と正しく不平を言い、 *cont*が使用されると、「シンボルの字句バインディングを使用している (CONTINUATIONS::名前は特殊変数の通常の命名規則 (*FOO* のような名前) に従いますが、動的バインディングではありません。"

何が起こるはずですか?

これがどのように機能するかを理解するには、 On Lispバージョンにあるすべての配管を持たない、より単純な実装を見る方がおそらく簡単です

(defparameter *restarts* '())

(defun do-restart ()
  (if (endp *restarts*) nil
      (funcall (pop *restarts*))))

(defun traverse-tree (k tree)
  (cond
    ((null tree) (do-restart))
    ((atom tree) (funcall k tree))
    (t (push (lambda () (traverse-tree k (cdr tree))) *restarts*)
       (traverse-tree k (car tree)))))

ここでは、継続渡しメカニズムや*restarts*リストを隠しません。これにより、次の動作が得られます。

CL-USER> (traverse-tree 'identity '((1 2) (3 4)))
1
CL-USER> (do-restart)
2
CL-USER> (do-restart)
3
CL-USER> (do-restart)
4
CL-USER> (do-restart)
NIL

複数リストのトラバーサルも再作成できます。期待どおりの結果が得られると思います。

CL-USER> (let ((k (lambda (num)
                    (traverse-tree (lambda (alpha)
                                     (list num alpha))
                                   '(a (b) c)))))
           (traverse-tree k '((1 2) 3)))
(1 A)
CL-USER> (do-restart)
(1 B)
CL-USER> (do-restart)
(1 C)
CL-USER> (do-restart)
(2 A)
CL-USER> (do-restart)
(2 B)
CL-USER> (do-restart)
(2 C)
CL-USER> (do-restart)
(3 A)
CL-USER> (do-restart)
(3 B)
CL-USER> (do-restart)
(3 C)
CL-USER> (do-restart)
NIL

ここでの違いは、継続をバインドするletのスコープを離れると意味が変わる*cont*への参照がないことです。

私の意見では、より良い実装は、継続を格納するために通常の字句 a 変数を使用するだけであり (上記のkのようなものですが、おそらくgensymによって生成された名前を使用します)、「継続を渡す関数の呼び出しは最終的には最も外側の継続を定義する=bindでラップされます。

于 2014-07-14T15:04:09.687 に答える