y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

define-modify-macro の謎

この文章は、 Lisp SETF Advent Calendar 2018 - Qiita の 12/7 分の記事としてかかれました。

簡単に define-modify-macro 概説

define-modify-macro というマクロがあります。 これを使うと、「ある場所の値を読み出し、関数を適用し、結果を書き戻す」という動きをするマクロを簡単に定義できます。

以下は Hyperspecの例です:

(define-modify-macro appendf (&rest args)
    append "Append onto list") =>  APPENDF
 (setq x '(a b c) y x) =>  (A B C)
 (appendf x '(d e f) '(1 2 3)) =>  (A B C D E F 1 2 3)

ある日書いたコード

define-modify-macro には関数名を渡すのですが、 define-modify-macro で作ったマクロを呼ぶと、関数の第一引数にその場所の元の値が渡されます。 上に出した append なら、そのままの順序で使えるのですが、変数の順序が合っていない関数もあったりします。

例えば、 acons という関数があり、以下のように使います:

(defparameter *some-alist* '((:hoge . 1)))
(acons :fuga 100 *some-alist*) ; => ((:FUGA . 100) (:HOGE . 1))

acons は関数なので、当然ながら第三引数に渡したリストは変更されません。 変更するなら、 setf などする必要があります:

(acons :fuga 100 *some-alist*) ; => ((:FUGA . 100) (:HOGE . 1))
*some-alist* ; => ((:HOGE . 1))  ; 変更されない。

(setf *some-alist* (acons :piyo 10000 *some-alist*))
*some-alist* ; => ((:PIYO . 10000) (:HOGE . 1))

上の setf を見ていると、「define-modify-macro にかけたい」とか思うわけですが、残念ながら acons がリストを取るのは第三引数です。 define-modify-macro は、第一引数に変更対象の値を渡しますから、引数の順番を並べかえたくなります。 というわけで、適当に lambdaかましてみたくなるのです:

(define-modify-macro aconsf (key val)
  (lambda (ref k v)
    (acons k v ref)))

実際に SBCL 1.4.6 (Mac) で試してみると、うまく動いているようでした:

(defparameter *some-alist* '((:hoge . 1)))

(aconsf *some-alist* :piyo 999)
*some-alist* ; => ((:PIYO . 999) (:HOGE . 1))

define-modify-macro の謎

ここで define-modify-macro の構文ですが、以下のようになっており:

define-modify-macro name lambda-list function [documentation] => name

そして、第三引数 function

function---a symbol.

とあるように、 symbol とされています。 つまり、上でやったようにラムダ式を与えるのは、なんと規格には書いてない挙動のようなのです。

しかし上では通ってしまっていて、 「処理系依存の現象?」と思いましたが、そうではないようで… 以下の実装は通ってしまうようです。

というわけで、なぜか色々な処理系で通るんだけど典拠はない、ような挙動を発見しました、ということでした。


(12/7 12:40頃 追記) コメントで頂きましたが、 LispWorks 7 だとエラーになるようです! やはり lambda を渡すのは可搬性がないようです。