“JRM’s Syntax-rules Primer for the Merely Eccentric” メモ(その4) is-symbol? について

続きです、 is-symbol? について let の変種…というか let を再実装するということで my-let を書きます。

(define-syntax my-let
  (syntax-rules ()
    ((my-let first-subform . other-subforms)
     (macro-if (is-symbol? first-subform)
       (expand-named-let () first-subform other-subforms)
       (expand-standard-let () first-subform other-subforms)))))

;; XXX: ここではクォートするだけにとどめます
(define-syntax expand-named-let
  (syntax-rules ()
    ((expand-named-let . e) '(expand-named-let . e))))

(define-syntax expand-standard-let
  (syntax-rules ()
    ((expand-standard-let . e) '(expand-standard-let . e))))

is-symbol? の定義があって、その後に注意を促す一文があります。

(define-syntax is-symbol?
  (syntax-rules ()
    ((is-symbol? k (form ...)) (return k #f))
    ((is-symbol? k #(form ...)) (return k #f))
    ((is-symbol? k atom)
     (letrec-syntax ((test-yes (syntax-rules () ((test-yes) (return k #t))))
                     (test-no (syntax-rules () ((test-no) (return k #f))))
                     (test-rule
                      (syntax-rules ()
                        ((test-rule atom) (test-yes))
                        ((test-rule . whatever) (test-no)))))
       (test-rule (#f))))))

We need to use LETREC-SYNTAX here because we do not want our continuation forms to be in the template of the test-rule; if the test rule matches, the atom we just tested would be rewritten!

これがピンと来ません。整理するために、 is-symbol? を元々の形にします。お手本はこれ。

(define-syntax symbol??
  (syntax-rules ()
    ((symbol?? (x ...) kt kf) kf)  ; It's a pair, not a symbol
    ((symbol?? #(x ...) kt kf) kf) ; It's a vector, not a symbol
    ((symbol?? maybe-symbol kt kf)
     (let-syntax
         ((test
           (syntax-rules ()
             ((test maybe-symbol t f) t)
             ((test x t f) f))))
       (test abracadabra kt kf)))))

これをもとにして、 test のテンプレート部分へ継続フォームを直接展開するようなコードを考えます。

(define-syntax is-symbol?
  (syntax-rules ()
    ((is-symbol? k (form ...)) (return k #f))
    ((is-symbol? k #(form ...)) (return k #f))
    ((is-symbol? k maybe-symbol)
     (let-syntax
         ((test
           (syntax-rules ()
             ((test maybe-symbol) (return k #t))
             ((test x) (return k #f)))))
       (test abracadabra)))))

;;
(my-let foo ((name value)) body)
;;=> (expand-named-let () abracadabra (((name value)) body))

脱線だけれども、 is-symbol? 内で、 test-rule マクロの呼び出しを (#f) を使っているのは、あえておかしな展開形になるフォームを渡すことで、不意にそのまま展開されたとしても、そこでエラーになるようにしようとしているのかな、と考えられます。シンボルそのままだと下手をしたら気付かない可能性があります。

さて、ここで test マクロの maybe-symbolis-symbol? マクロのパターン変数なので、入力内容の部分形式で置き換えられます。 (my-let foo ((name value)) body) を例として考えます。

(let-syntax
    ((test
      (syntax-rules ()
        ((test foo) ...)
        ...)))
  (test abracadabra))

(test foo) のテンプレート部を考えると、

(letrec-syntax
    ((test
      (syntax-rules ()
        ((test foo)
         (return ((if-decide)
                  (expand-named-let . (() foo (((name value)) body)))
                  (expand-standard-let . (() foo (((name value)) body))))
                 #t))
        ...)))
  (test abracadabra))

識別子 foo が含まれています。これはパターン変数です。 is-symbol? マクロに渡した継続フォーム内の footest マクロ呼び出し時の部分形式で置き換えられてしまいます。

test マクロのテンプレート部分に継続フォームが現れないようにすれば上の問題は回避できます。というわけで下のようになる、と。

(define-syntax is-symbol?
  (syntax-rules ()
    ((is-symbol? k (form ...)) (return k #f))
    ((is-symbol? k #(form ...)) (return k #f))
    ((is-symbol? k maybe-symbol)
     (let-syntax
         ((test-sk (syntax-rules () ((test-sk) (return k #t))))
          (test-fk (syntax-rules () ((test-fk) (return k #f)))))
       (let-syntax
           ((test
             (syntax-rules ()
               ((test maybe-symbol) (test-sk))
               ((test . x) (test-fk)))))
         (test (#f)))))))

最初にピンと来なかったと書いたのは、一見大丈夫そうだな、と思ったということで、もっというと手を動かすまでは注意書きを読んでも、その意味するところがわからなかった、というのが正確です。つまり、マクロに現れる k は任意のフォームで、パターン変数となるものが含まれることがある、というのに気付かなかったということなのだけれども、これあらかじめ気付くことができる、もしくはできるようになるだろうか…