2009年10月2日金曜日

elisp: 関数渡しの憂鬱

emacs lispにおいて、関数渡しを行う際の落とし穴。

例えば、
次のような関数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において、 dataslstに書き換えると、
  (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 件のコメント:

コメントを投稿