#:g1: マクロ禁止令

Posted 2017-12-13 11:52:38 GMT

Lisp Advent Calendar 2017 十三目です。
空きがあったのでネタで埋めようと思い書きました。
十三目書いてたのに!という方は、すいませんが、まだ空きがあるので他の日を埋めてください。

マクロが禁止されたらどうなるの

良くも悪くも誤解が多いLispマクロ。
Lispマクロに心酔するあまり過大評価する人もいれば、過大評価する人をみて過小評価に転ずる人もいる始末ですが、基本的にはコードを生成するだけの機能です。

そんなマクロですが、Common Lispで仮に禁止されたらどうやって生きていったら良いのか考えてみました。
(ちなみに話を簡単にするためにローカルマクロのことは考えないことにします。)

defmacroは何をしているの

Common Lispのdefmacroで定義するものは、リストを引数にして、リストを返すという関数です。
しかし、評価の前に再帰的にマクロを展開するフェイズがあり、そこで展開関数が実行されるので、まるで関数評価のような感じで使うことができます。

例えば、下記のようなコードでloopの展開関数だけ実行することも可能です。

(funcall (macro-function 'loop)
         '(loop :for i :from 0 :repeat 10 :collect i)
         nil)

関数だけでdefmacroのようなことをしてみる

例としてdotimesのようなものを考えてみましょう

'(dotimes (i 10) (princ i))

のようなリストを

'(prog ((#:|limit17589| 10) (i 0))
      "=>"
      (cond ((<= #:|limit17589| i) (return (let ((i nil)) nil))))
      (progn (princ i))
      (incf i)
      (go "=>"))

のようなリストに変形すれば良いので、

(defun mydotimes (form &optional env)
  (declare (ignore env))
  (destructuring-bind (_ (var limit &optional result) &body body)
                      form
    (declare (ignore _))
    (let ((limvar (gensym "limit"))
          (tag (gensym "=>")))
      `(prog ((,limvar ,limit)
              (,var 0))
        ,tag (cond ((<= ,limvar ,var)
                    (return (let ((,var nil)) ,result))))
             (progn ,@body)
             (incf ,var)
             (go ,tag)))))

のような関数を書けるでしょう。
(まあ、結局の所マクロを書く作法が身に付いていないと、こういう関数も書けないのですがそれは一旦忘れましょう)

これで、下記のように書けます。

(with-output-to-string (out)
  (declare (special out))
  (eval (mydotimes 
         `(mydotimes (i 3)
            ,(mydotimes '(mydotimes (j 3) 
                           (princ i out)
                           (princ j out)))))))
→ "000102101112202122" 

やはりマクロ展開と実行コードを混ぜて書かないといけないので、ごちゃごちゃしてしまいます。
別個にマクロ展開関数を用意して、オペレーターが定義したマクロかどうかを確認しつつ展開するようにすれば、

(with-output-to-string (out)
  (declare (special out))
  (mexpand `(mydotimes (i 3)
              (mydotimes (j 3) 
                (princ i out)
                (princ j out)))))
→  "000102101112202122" 

位には圧縮できるかもしれません。

もうちょっと綺麗にできないか

とりあえずは、安直に簡単に見た目を変える方向で、リーダーマクロを使ってごちゃごちゃを隠してみましょう。
見た目がごちゃごちゃしているだけではなく、上記では、変数の結合も実行時にしているので、変数をダイナミック変数に指定していたりします。この辺りもリーダーマクロで読み取り時に展開してしまえば解決です。

なお、リーダーマクロも禁止ならファイルを2パスで処理する等々しかないですね。

(set-syntax-from-char #\] #\))
(set-macro-character 
 #\[ 
 (lambda (s c)
   (declare (ignore c))
   (let ((form (read-delimited-list #\] s T)))
     (funcall (car form) form))))

(with-output-to-string (out)
  [mydotimes (i 3)
    [mydotimes (j 3)
      (princ i out)
      (princ j out)]])
→ "000102101112202122" 

結論

結局の所、

の2点が言語に備わっていれば、Lispマクロのような機能と使い勝手は実現可能だということが分かるでしょうか。
特にLispには限らない筈ですが、使い勝手を含めて真面目に活用が考えられてきた、また実績があるのは、ほぼLisp系言語のみ、というのが現状だと思います。

誕生当初は、LispもM式→S式の変換をして実行するものと考えられていたLispですが、S式というデータの世界にLispプログラマが飛び込んだことが偉大だったのかもしれません。


HTML generated by 3bmd in LispWorks 7.1.0

comments powered by Disqus