リストとしての関数 -> 簡易クロージャの実装

最近 Lisp がお気に入りです。

今日は、Lisp プログラムがリスト (括弧で括られた式) として表記される、という当たり前の事実について、Emacs Lisp をベースにしながら考察してみたいと思います。

関数はリスト、リストは関数

古い Lisp では、関数は第一要素に "lambda" というシンボルを持つ、というだけの単なるリストであったそうです。

新しい Lisp (SchemeCommon Lisp) では「関数」という独立した型になっていますが、Emacs Lisp では今も古い時代の性質を残しているということで、この点を Common Lisp と対比しながら検証してみます。

lambda 式 (関数) を、そのままの形と、二通りのクォート式で括った形、それぞれについてテストします (Meadow 3.00 と GNU CLISP 2.41 を使用しています)。

lambda 式:

;; emacs lisp
(listp (lambda () ()))              ; => t
;; common lisp
(listp (lambda () ()))              ; => NIL

;; emacs lisp
(functionp (lambda () ()))          ; => t
;; common lisp
(functionp (lambda () ()))          ; => T

クォート式 (引数を評価せずそのまま返す):

;; emacs lisp
(listp '(lambda () ()))             ; => t
;; common lisp
(listp '(lambda () ()))             ; => T

;; emacs lisp
(functionp '(lambda () ()))         ; => t (1)
;; common lisp
(functionp '(lambda () ()))         ; => NIL

シャープクォート式 (引数を関数オブジェクト化する):

;; emacs lisp
(listp #'(lambda () ()))            ; => t (2)
;; common lisp
(listp #'(lambda () ()))            ; => NIL

;; emacs lisp
(functionp #'(lambda () ()))        ; => t
;; common lisp
(functionp #'(lambda () ()))        ; => T

実際のところ、一番上の lambda 式における "lambda" というのは関数オブジェクトを返すマクロで、「シャープクォート」で括るのと同じ意味になります (なので全く同じ結果が表示されています)。

以上の結果、Emacs Lisp においては

  1. シンボル "lambda" を第一要素に持つリストは関数である
  2. 関数オブジェクトはリストでもある

ことが確認できました。


さて、この「リストが関数になる」という前者の性質を利用すると、以下の様にマクロっぽい仕方で関数を構成することが可能になります。

;; emacs lisp
(funcall (list 'lambda '(x) '(+ x 1))
         1)
; => 2

シンボルを並べただけのリストが "funcall" で関数として呼べてしまうわけです (CL では出来ないことです)。

クロージャもどき

応用を進めましょう。動的に関数を定義する関数を作ってみます:

;; emacs lisp
(defun define-adder (sym n)
  (fset sym
        (list 'lambda '(x) (list '+ 'x n))))

「引数に n を加える関数」を作る関数です。

elisp にはクロージャが無いため、define-adder への引数を lambda 式の中でそのまま参照することが出来ないのです ("void variable n" というエラーが出ます)。なので、n の値を予め展開してから関数式を組み立てています。

lambda 式を返すだけでも良いんですが、それだと呼ぶ時に funcall か apply が必要になるため、fset を使って関数として定義するようにしました。

尚、バッククォートを使ってより簡単に書くことも出来ます:

;; emacs lisp
(defun define-adder (sym n)
  (fset sym
        `(lambda (x) (+ x ,n))))

実行例です:

(define-adder 'add1 1)
(add1 1)                            ; => 2

引数に 1 を加える関数が動的に定義されました。

ちなみに、同じことを CL でしようとすると型変換が必要になります:

;; common lisp
(defun define-adder (sym n)
  (setf (symbol-function sym)
        (coerce `(lambda (x) (+ x ,n)) 'function)))

もっとも、CL には本物のクロージャがありますから、こんな面倒な事をする必要はありません:

;; common lisp
(defun define-adder (sym n)
  (setf (symbol-function sym)
        #'(lambda (x) (+ x n))))

引数束縛 (currying) 関数

クロージャが無い elisp でもクロージャらしきものが作れることが分かったところで、curry 関数を実装してみましょう。

;; emacs lisp
(defun curry (fn &rest fixed)
  `(lambda (&rest args)
     (apply #',fn (append ',fixed args))))

固定したい引数を予め展開してセットした関数式を返しています。

改めて強調しますが、この関数が返しているのはあくまでも ("lambda" を先頭に含む) リストであり、それが関数として呼び出し可能となる elisp の性質を活用してみよう、というわけです。

では、3 引数を加算する関数で試してみましょう:

(defun add3 (x y z)
       (+ x y z))
(funcall (curry 'add3 1) 2 3)	    ; => 6
(funcall (curry 'add3 1 2) 3)       ; => 6

上手くいきました。

関数合成 (compose) 関数

最後に、関数合成にもトライしてみます。

;; emacs lisp
(defun compose (&rest flist)
  `(lambda (&rest args)
     (dolist (f
              '(,@(reverse flist))
              (car args))
       (setq args
             (list (apply f args))))))

引数の関数リストを逆順にして、関数式の中に埋め込んでいるのがポイントです。基本的には Mochikit の compose の定義方法を真似ました。

ダイナミック cadr (2 番目の要素を取得):

(funcall (compose 'car 'cdr)
	 '(a b c d))
; => b

上手くいったようです。

補足

[20070417]:
敢えてマクロについて触れなかったんですが、上で紹介した curry および compose は、"defun" を "defmacro" に置き換えることでそのままマクロ定義となります (コンパイルによる実行効率を考えるならば、マクロにすべきでしょうすいません、自信有りません)。その場合、呼び出し時に関数名シンボルの引用符を外す必要があります。例:

(funcall (compose car cdr)
         '(a b c d))


[20070419]:
curry が関数を固定することが出来なかったので修正しました。固定する引数のクォートを忘れていたのが原因だったようです。変更点:
",@fixed args" -> "(append ',fixed args)"

これにより、例えば "mapcan" は次のように定義することができます:

;; ポール・グレアム "ANSI Common Lisp" (http://paulgraham.com/acl.html) 6.6 より
(fset 'mapcan
      (compose (curry #'apply #'nconc)
               #'mapcar))

例 1:

(mapcan #'identity '((a) (b) (c)))
; => (a b c)        

例 2:

;; http://www.apl.jhu.edu/~hall/lisp/Lisp-FAQ-1.51.text: Lisp Idioms
(defun evenp (n) (zerop (% n 2)))

(mapcan #'(lambda (x) (when (and (numberp x) (evenp x)) (list x)))
	'(1 2 3 4 x 5 y 6 z 7))
; => (2 4 6)


[20070502]:

上で定義した `mapcan' を利用して `filter' を定義してみたところ

(defun filter (fn args)
  (mapcan (lambda (x)
            (if (funcall fn x)
                (list x)
              nil))
          args))

実行時エラーが出ました。`fn' とした変数に意図しないものが入ってしまっていて、いわゆる変数キャプチャの問題でした。compose を次のように変更する必要があります:

(defun compose (&rest fns)
  (let ((gfn (make-symbol "fn")))
    `(lambda (&rest args)
       (dolist (,gfn
                '(,@(reverse fns))
                (car args))
         (setq args
               (list (apply ,gfn args)))))))

もうどう見てもマクロです。