“JRM’s Syntax-rules Primer for the Merely Eccentric” メモ(番外その3) id-eq??, id-eqv?? について

ここでおまけとし出てくるものです。

どちらも説明とコードが非常に興味深いです。今感じているおもしろポイントを忘れないように、考えを整理するためにも読み解くメモ。

まず、 id-eq?? から。

(define-syntax id-eq??
  (syntax-rules ()
    ((id-eq?? id b kt kf)
     (let-syntax
         ((id (syntax-rules ()
                ((id) kf)))
          (ok (syntax-rules ()
                ((ok) kt))))
       (let-syntax
           ((test (syntax-rules ()
                    ((_ b) (id)))))
         (test ok))))))

マクロ展開して様子を見ます。

(id-eq?? A B #t #f)
;;->
(let-syntax
    ((A (syntax-rules ()
          ((A) #f)))
     (ok (syntax-rules ()
           ((ok) #t))))
  (let-syntax
      ((test (syntax-rules ()
               ((_ B) (A)))))
    (test ok)))
;;=> #f

(id-eq?? A A #t #f)
;;->
(let-syntax
    ((A (syntax-rules ()
          ((A) #f)))
     (ok (syntax-rules ()
           ((ok) #t))))
  (let-syntax
      ((test (syntax-rules ()
               ((_ A) (A)))))
    (test ok)))
;;=> #t

一見なぜ ok が呼び出されるのか納得がいきません。マクロ展開の形を見るに、テンプレート部には、 (A) というマクロ呼び出しが見えるから、常に A マクロが呼び出されるように感じられます。(展開形ではなく、元々のやつでも id と見えているマクロ呼び出しに、常に展開されるのでは?と思ってしまう。)

注目しなければならないのは、パターン変数がテンプレート部に現れたらば入力内容の部分形式で置き換えられる、ということです。これが意識できると、 #t に展開される下の場合、 A マクロは呼び出されない、替わりに ok が呼び出されるのが納得できます。

元々のマクロで言うところの、 idb はパターン変数で、テンプレート部に現れた場合には入力内容の部分形式で置き換えられる、ということです。

    ...
       (let-syntax
           ((test (syntax-rules ()
                    ((_ b) (id)))))
         (test ok))
    ...

仮に bid が字面上同じだったとしても、パターン変数に現れた識別子がテンプレート部にも現れたことにならない限り、 ok マクロの呼び出しに展開されません。その意味で bid が同じ場合に限り kt へ展開されます。

ここが私には特におもしろく感じたポイントです。二つの識別子がこの意味で同じかどうかをテストするのに、文字通りパターン変数としてテンプレート部にも現れたかどうかで判断しようとしていて、それをそのままマクロでそのようなコードにして処理系に丸投げしている所です。

では id-eqv?? を見ます。(展開形も同時に挙げます。)

(define-syntax id-eqv??
  (syntax-rules ()
    ((id-eqv?? a b kt kf)
     (let-syntax
         ((test (syntax-rules (a)
                  ((test a) kt)
                  ((test x) kf))))
       (test b)))))

(id-eqv?? A B #t #f)
;;->
(let-syntax
    ((test (syntax-rules (A)
             ((test A) #t)
             ((test x) #f))))
  (test B))
;;=> #f

(id-eqv?? A A #t #f)
;;->
(let-syntax
    ((test (syntax-rules (A)
             ((test A) #t)
             ((test x) #f))))
  (test A))
;;=> #t

こちらは展開形を素直に見ると、それぞれの識別子は、一方がリテラル部に現れる意味で同じ場合に #t に展開されます。

ここも同様なおもしろポイントです。 syntax-rules へリテラル部が渡されてマッチしたのかどうかということを、そのままマクロでそのようなコードにしている、ということです。

示されるテストコードはこちらです。

(define-syntax mfoo
  (syntax-rules ()
    ((mfoo tester a)
     (tester foo a 'yes 'no))))

(begin
  ;; expected answer: (id-eq??:  no no)
  (display
   (list "id-eq??: "
         (mfoo id-eq?? foo)
         (let ((foo 1)) (mfoo id-eq?? foo))))
  (newline))

(begin
  ;; expected answer: (id-eqv??:  yes no)
  (display
   (list "id-eqv??: "
         (mfoo id-eqv?? foo)
         (let ((foo 1)) (mfoo id-eqv?? foo))))
  (newline))

自分なりに注釈を入れるとしたら以下のようなことになります。

(mfoo id-eq?? foo)
;;=> 'no
;; 字面は `foo` だけれども異なる展開ステップで現れた識別子の比較なので `'no`

(let ((foo 1)) (mfoo id-eq?? foo))
;;=> 'no
;; 上と同様

(mfoo id-eqv?? foo)
;; 字面が `foo` でどちらも未束縛の識別子の比較なので `'yes`
;; 仮に `foo` が `mfoo` から見えている場合、グローバルな束縛であると考えれば、
;; 例えばあらかじめ `(define foo ...)` されているとすれば `'yes`。
;; `syntax-rules` のリテラル部の扱いを考えると納得できます。

(let ((foo 1)) (mfoo id-eqv?? foo))
;;=> 'no
;; 字面が `foo` ですが、`mfoo` マクロ呼び出し元は `let` で、束縛がある識別子、
;; `mfoo` マクロ内では未束縛(もしくは、マクロ呼び出し元の `foo` とは異なる
;; 場所に束縛されている)となる識別子なので '`no`

ただ私の感覚では、典型的な syntax-rules のマクロのコードとは言えないんじゃないかと感じられるためか(あくまでも私の感覚ではです。何というか foo が唐突に出て来るところが典型的でないように感じられてしまいました)かえってピンときませんでした。(同じことなのだけれども) multiple-value-set! でやっていたように (https://t.laafc.net/2018/05/24/syntax-rules-primer.html) 展開ステップ毎に識別子を累積するようなコードで再確認します。

(let-syntax
    ((eq?? (syntax-rules () ((eq?? a b) (id-eq?? a b #t #f))))
     (eqv?? (syntax-rules () ((eqv?? a b) (id-eqv?? a b #t #f)))))
  (letrec-syntax
      ((test (syntax-rules ()
               ((test (t0 t1) (tt0 tt1))
                ;; t0, t1, tt0, そして tt1 いずれも字面は `tmp`
                (list
                 ;; 異なる展開ステップで作られた識別子を比較
                 (eq?? t0 t1)   ;; #f
                 (eqv?? t0 t1)  ;; #t
                 ;; 同じ展開ステップで作られた識別子を比較
                 ;; 正確には別の展開ステップでコピーした識別子
                 (eq?? t1 tt1)
                 (eqv?? t1 tt1)))
               ((test (tmp0 tmp1) ())
                (test (tmp0 tmp1) (tmp0 tmp1)))
               ((test (e ...) a)
                (test (e ... tmp) a)))))
    (test () ())))
;;=> (#f #t #t #t)

結局 tmp が唐突に出現するので自分でも何を問題として典型的かどうかを考えているのかよくわかりません。私が読んで/書いている量が少ないので慣れていない、ということなのだろう…