Edited at

PythonistaがCommon Lispでクロージャを書くときによくやる間違い

More than 1 year has passed since last update.

メインでPythonを書いていてCommon Lispをたまに書くのですがクロージャを書くときによくやる間違いがあります.

下のような簡単なカウンターを例に取ります.


counter.py

def counter():

n = 0

def inner():
nonlocal n
n += 1
return n

return inner


これは以下のように使用します.


counter.pyの使用例

c1 = counter()

print(c1()) # 1
print(c1()) # 2
print(c1()) # 3

c2 = counter()
print(c2()) # 1
print(c2()) # 2
print(c2()) # 3

c1c2でそれぞれ別の環境を持てていることがわかります.

これをCommon Lispにするとどうなるかというのが本題です.defはCommon Lispではdefun,局所変数の宣言はlet,変数の代入はsetfという風に置き換えれば以下のようになります.


counter.lisp?

(defun counter ()

(let ((n 0))
(defun inner ()
(setf n (1+ n)))))

一見問題なさそうですがこれを先ほどのPythonの例と同じように使用すると間違いに気づきます.


counter.lisp?の使用例

(defvar c1 (counter))

(format t "~a~%" (funcall c1)) ;; 1
(format t "~a~%" (funcall c1)) ;; 2
(format t "~a~%" (funcall c1)) ;; 3

(defvar c2 (counter))

(format t "~a~%" (funcall c2)) ;; 4
(format t "~a~%" (funcall c2)) ;; 5
(format t "~a~%" (funcall c2)) ;; 6


c1c2で環境が共有されてしまっています….これはdefunが常にトップレベルに関数を定義するからです.この点がPythonと異なる挙動の原因です.

正しくはlambdaで返すのが正解です."Let Over Lambda"ですね.正しいコードは以下になります.


counter.lisp

(defun counter ()

(let ((n 0))
(lambda inner ()
(setf n (1+ n)))))

単純ですがよくやる間違いでそのたびに悩まされるのでCommon Lispでクロージャを書くときは頭の中で"Let Over Lambda"を連呼しています.


追記

コメントを頂いたので補足的な内容を.

Common Lispにはfletlabelといった局所関数を定義するための構文が用意されています.今回の例は以下のようにも書けるようです.


g000001さんのコメントより引用

(defun counter ()

(let ((n 0))
(labels ((inner ()
(setf n (1+ n))))
#'inner)))

(defun counter ()
(let ((n 0))
(flet ((inner ()
(setf n (1+ n))))
#'inner)))


ちなみに今回改めて調べて見てわかったのですが,fletlabelsには参照するスコープに自分自身が含まれるかどうかという違いがあるそうです.CLHSに以下のような記述がありました.


labels is equivalent to flet except that the scope of the defined function names for labels encompasses the function definitions themselves as well as the body.


ちなみにPythonではlambdaが式を返せないので最初に書いたCommon Lispのようには行きませんが,その気になればすべてlambdaで書くこともできます.恐ろしく黒魔術なので実用性は皆無です.


lambdaでcounter.py

(lambda:

(globals().update(
counter=
(lambda:
(lambda _:
(lambda:
(_.append(0), len(_))[1]))([])))))()