emacs lispにおいて、関数渡しを行う際の落とし穴。
例えば、
次のような関数my-findを定義したとする。
次のような関数my-findを定義したとする。
(defun my-find (pred lst) (if lst (if (funcall pred (car lst)) (car lst) (my-find pred (cdr lst))) nil))
my-findは、リストの中からある条件に一致した要素をみつける関数で、
次のように使える。
例1: 奇数を探す
(my-find (function oddp) '(0 2 -2 3 4)) ; ==> 3
例2: 負の数を探す場合
(my-find #'(lambda (x) (< x 0)) '(0 2 -2 3 4)) ; ==> -2
例3: datasの最初の要素より大きな数を探す場合
(let ((datas '(3 4 5))) (my-find #'(lambda (x) (< (car datas) x)) '(0 2 -2 3 4))) ; ==> 4
しかし、最後の例3において、 datas を lstに書き換えると、
(let ((lst '(3 4 5)))
(my-find #'(lambda (x) (< (car lst) x)) '(0 2 -2 3 4)))
; ==> nil
となり、これは期待した結果ではない!何故?
emacs lispの変数束縛は、動的バインディングという方式なので、
(lambda (x) (< (car lst) x))
の lstは、定義された場所ではなく使われる場所で評価される。すなわち、
定義された場所の '(3 4 5) ではなく、使われる場所、この場合は関数 my-find
(defun my-find (pred lst) ... )
の第2引数として渡されるlstの値に束縛されるからだ。ということは、my-findのような関数を引数とする関数を使う場合、
変数の名前がぶつかってしまうと、
意図しない結果を引き起こしてしまう恐れがあるということだ。
さて困った!
この問題を解決するにはどうすればいいのだろう? 僕にはわからない。
とりあえずは、次の二つを心がけるしかないのか。
- (function (lambda (x) ... ) で関数を渡す時には、なるべく、lambda外部の変数 をlambda内部で使わないようにする。
- 引数に関数を受け取る関数の定義においては、 なるべく変数を特殊な名前で書くようにする。
ex. (defun my-find (@pred __lst) ... )
ちなみに、静的バインディングで変数を束縛する、
common lisp や scheme では、定義された場所の環境で変数を評価するから、
このような問題は起こらない。
$ clisp [1]> (defun my-find (pred lst) (if lst (if (funcall pred (car lst)) (car lst) (my-find pred (cdr lst))) nil)) [2]> (let ((lst '(3 4 5))) (my-find #'(lambda (x) (< (car lst) x)) '(0 2 -2 3 4))) ==> 4
追記 (2009年 10月14日 水曜日)
lexical-let というのを使えば解決できることがわかった。
(lexical-let ((lst '(3))) (my-find #'(lambda (x) (> x (car lst))) '(1 2 3 4 5))) ==> 4
lexical-letは名前のとおり、
レキシカルなletバインディングをエミュレートしてくれるマクロだ。
今まで僕が知らなかっただけで、結構有名な機能らしい。
wikipedia
にも記述がある。
これを使えばemacs lispでクロージャーを書くことも出来る。
(defun counter-new (n) (lexical-let ((n n)) #'(lambda (cmd) (cond ((eq cmd :get) (setq n (+ n 1)) n) ((eq cmd :peek) n) (t (error (format "%s: illegal command" cmd))))))) (setq c (counter-new 0)) (setq c2 (counter-new 100)) (funcall c :get) ;==> 1 (funcall c2 :get) ;==> 101 (funcall c :get) ;==> 2 (funcall c :peek) ;==> 2
もう何十年もemacsと付き合っているが、まだまだ知らない事だらけだ.....
0 件のコメント:
コメントを投稿