如何复制使用 Lisp 闭包制作的计数器?
How can I copy a counter made using a Lisp closure?
Lisp 闭包的经典示例是以下 return 计数器的函数:
(defun make-up-counter ()
(let ((n 0))
#'(lambda () (incf n))))
当被调用时,它会增加它的计数并且 returns 结果:
CL-USER > (setq up1 (make-up-counter))
#<Closure 1 subfunction of MAKE-UP-COUNTER 20099D9A>
CL-USER > (funcall up1)
1
CL-USER > (funcall up1)
2
当我把这个展示给一个不熟悉 Lisp 的朋友时,他问我如何复制一个计数器来创建一个新的、独立的相同类型的计数器。这不起作用:
CL-USER > (setq up2 up1)
#<Closure 1 subfunction of MAKE-UP-COUNTER 20099D9A>
因为 up2 不是新计数器,它只是同一计数器的不同名称:
CL-USER > (funcall up2)
3
这是我的最佳尝试:
(defun make-up-counter ()
(let ((n 0))
#'(lambda (&optional copy)
(if (null copy)
(incf n)
(let ((n 0))
#'(lambda () (incf n)))))))
到 return 你用参数 t:
调用它的计数器的副本
(defun copy-counter (counter) (funcall counter t))
它适用于第一代副本:
CL-USER > (setq up2 (copy-counter up1))
#<Closure 1 subfunction of MAKE-UP-COUNTER 200DB722>
CL-USER > (funcall up2)
1
但是如果你尝试复制up2显然是行不通的。我看不出如何让它正常工作,因为 make-up-counter 的定义需要在它自己的定义中有一个它自己的副本。
有什么建议吗?
要解决这个问题,你需要使用递归函数,使用labels
:
(defun make-up-counter ()
(labels ((new ()
(let ((n 0))
(lambda (&optional copy)
(if copy
(new)
(incf n))))))
(new)))
当 copy
为真时,您甚至可以让它复制当前计数器值:
(defun make-up-counter ()
(labels ((new (n)
(lambda (&optional copy)
(if copy
(new n)
(incf n)))))
(new 0)))
为了两全其美,如果 copy
是数字,您可以让它创建一个具有指定值的计数器,否则如果为真则复制计数器值,否则递增:
(defun make-up-counter ()
(labels ((new (n)
(lambda (&optional copy)
(cond ((numberp copy) (new copy))
(copy (new n))
(t (incf n))))))
(new 0)))
这是另一个解决方案,其中 copy-counter
获取一个计数器作为参数,returns 一个从参数的当前值开始的新计数器,但使用另一个变量:
(defun new-counter(&optional (n 0))
(lambda (&optional noincrement)
(if noincrement n (incf n))))
(defun copy-counter(c)
(new-counter (funcall c t)))
这是一个测试:
CL-USER> (let* ((up1 (new-counter))
(up2 (progn (funcall up1) (funcall up1) (copy-counter up1))))
(print (funcall up2))
(print (funcall up2))
(print (funcall up2))
(print (funcall up1))
"end test")
3
4
5
3
"end test"
解决此问题的最简单方法是 make-up-counter
取一个数字进行计数。
(defun make-up-counter (&optional (initial-count 0))
(let ((count initial-count))
#'(lambda () (incf count))))
但这并不能解决问题,因为我们无法检查计数器中的值,因此如果我们尝试使用计数器来为值设定种子,我们将修改当前计数器。
(defun copy-counter (counter)
(make-up-counter (funcall counter)))
为了提供一种获取值的方法,我们使闭包采用 'operation' 参数,因此我们可以检查或增加值
(defun make-up-counter (&optional (initial-count 0))
(let ((count initial-count))
#'(lambda (&optional (operation :increment))
(ecase operation
(:inspect count)
(:increment (incf count))))))
(defun copy-counter (counter)
(make-up-counter (funcall counter :inspect)))
现在当我们运行下面的代码
(let ((1st-counter (make-up-counter)))
(loop :repeat 3 :do (funcall 1st-counter))
(let ((2nd-counter (copy-counter 1st-counter)))
(loop :repeat 3 :do (funcall 1st-counter))
(format t "1st counter: ~A~%2nd counter: ~A~%"
(funcall 1st-counter :inspect)
(funcall 2nd-counter :inspect))))
它打印
1st counter: 6
2nd counter: 3
没有真正回答问题。但这样一来,副本会更容易...
(defun make-up-counter ()
(let ((n 0))
#'(lambda () (incf n))))
一般来说,我会避免将此类代码的更复杂版本用于可维护软件的生产用途。调试和自省更难。这是过去的基本 FP 知识(使用闭包来隐藏可变状态,例如参见早期的 Scheme 论文),但对于任何更复杂的事情来说,这都是一种痛苦。它隐藏了值——这很有用——但同时也使得调试变得困难。最小值是 debugger/inspector 能够查看闭包绑定。方便,因为好写,但是后面要付出代价
问题:
CL-USER 36 > (make-up-counter)
#<anonymous interpreted function 40600015BC>
这是什么?它的功能与其他所有功能一样。它没有说明它的目的、它的参数、文档、来源,它没有文档化的界面,没有有用的打印表示,代码在使用时不能更新,......我们可以向它添加更多功能 - 在内部 -但是我们可以从像 CLOS 这样的对象系统中免费获得所有这些。
(defclass counter ()
((value :initarg :start :initform 0 :type integer)))
(defmethod next-value ((c counter))
(with-slots (value) c
(prog1 value
(incf value))))
(defmethod copy-counter ((c counter))
...)
(defmethod reset-counter ((c counter))
...)
...
然后:
CL-USER 44 > (let ((c (make-instance 'counter :start 10)))
(list (next-value c)
(next-value c)
(next-value c)
c))
(10 11 12 #<COUNTER 40200E6F3B>)
CL-USER 45 > (describe (fourth *))
#<COUNTER 40200E6F3B> is a COUNTER
VALUE 13
是正确的,你应该避免使用闭包作为对象。您可以使用已经显示的 defclass
,但是对于一个简单的计数器,您可以简单地写:
(defstruct counter (value 0))
这定义了您需要的所有内容:(make-counter)
、(make-counter :value x)
和 (copy-counter c)
都按预期工作。您的对象也可以打印成可读的。
(let* ((c (make-counter))
(d (copy-counter c)))
(incf (counter-value c))
(values c d))
#S(counter :value 1)
#S(counter :value 0)
您仍然应该导出更高级别的函数,例如 reset 和 next 这样您的计数器的用户就不需要知道它是如何实现的已实施。
我们可以做的是定义一个 API 来构造一个计数器,这样:
(make-counter <integer>) --> yields new counter starting at <integer>
(make-counter <counter>) --> yields a clone of counter
诀窍在于我们还为计数函数(计数器本身)提供了一个可选参数。如果该参数是 nil
,那么它才算数。否则它会克隆自己。这只是基本的 OOP:计数器是一个对象,它有两个方法。当 make-counter
API 被要求克隆计数器时,它委托给副本 "method".
(defun make-counter (&optional (constructor-arg 0))
(etypecase constructor-arg
(integer
(lambda (&optional opcode) ;; opcode selects method
(ecase opcode ;; method dispatch
((nil) (prog1 constructor-arg (incf constructor-arg)))
(copy-self (make-counter constructor-arg)))))
(function
(funcall constructor-arg 'copy-self))))
测试运行:
[1]> (defvar x (make-counter 3))
X
[2]> (funcall x)
3
[3]> (funcall x)
4
[4]> (defvar y (make-counter x))
Y
[5]> (funcall x)
5
[6]> (funcall x)
6
[7]> (funcall x)
7
[8]> (funcall y)
5
[9]> (funcall y)
6
[10]> (funcall x)
8
[11]> (funcall y)
7
与其以这种方式重载 make-counter
构造函数,不如将其拆分为两个函数 API:
(defun make-counter (&optional (constructor-arg 0))
(lambda (&optional opcode)
(ecase opcode
((nil) (prog1 constructor-arg (incf constructor-arg)))
(copy-self (make-counter constructor-arg)))))
(defun copy-counter (counter)
(funcall constructor-arg 'copy-self))
copy-counter
只不过是对象上基于操作码的低级调度 API 的包装器。
Lisp 闭包的经典示例是以下 return 计数器的函数:
(defun make-up-counter ()
(let ((n 0))
#'(lambda () (incf n))))
当被调用时,它会增加它的计数并且 returns 结果:
CL-USER > (setq up1 (make-up-counter))
#<Closure 1 subfunction of MAKE-UP-COUNTER 20099D9A>
CL-USER > (funcall up1)
1
CL-USER > (funcall up1)
2
当我把这个展示给一个不熟悉 Lisp 的朋友时,他问我如何复制一个计数器来创建一个新的、独立的相同类型的计数器。这不起作用:
CL-USER > (setq up2 up1)
#<Closure 1 subfunction of MAKE-UP-COUNTER 20099D9A>
因为 up2 不是新计数器,它只是同一计数器的不同名称:
CL-USER > (funcall up2)
3
这是我的最佳尝试:
(defun make-up-counter ()
(let ((n 0))
#'(lambda (&optional copy)
(if (null copy)
(incf n)
(let ((n 0))
#'(lambda () (incf n)))))))
到 return 你用参数 t:
调用它的计数器的副本(defun copy-counter (counter) (funcall counter t))
它适用于第一代副本:
CL-USER > (setq up2 (copy-counter up1))
#<Closure 1 subfunction of MAKE-UP-COUNTER 200DB722>
CL-USER > (funcall up2)
1
但是如果你尝试复制up2显然是行不通的。我看不出如何让它正常工作,因为 make-up-counter 的定义需要在它自己的定义中有一个它自己的副本。
有什么建议吗?
要解决这个问题,你需要使用递归函数,使用labels
:
(defun make-up-counter ()
(labels ((new ()
(let ((n 0))
(lambda (&optional copy)
(if copy
(new)
(incf n))))))
(new)))
当 copy
为真时,您甚至可以让它复制当前计数器值:
(defun make-up-counter ()
(labels ((new (n)
(lambda (&optional copy)
(if copy
(new n)
(incf n)))))
(new 0)))
为了两全其美,如果 copy
是数字,您可以让它创建一个具有指定值的计数器,否则如果为真则复制计数器值,否则递增:
(defun make-up-counter ()
(labels ((new (n)
(lambda (&optional copy)
(cond ((numberp copy) (new copy))
(copy (new n))
(t (incf n))))))
(new 0)))
这是另一个解决方案,其中 copy-counter
获取一个计数器作为参数,returns 一个从参数的当前值开始的新计数器,但使用另一个变量:
(defun new-counter(&optional (n 0))
(lambda (&optional noincrement)
(if noincrement n (incf n))))
(defun copy-counter(c)
(new-counter (funcall c t)))
这是一个测试:
CL-USER> (let* ((up1 (new-counter))
(up2 (progn (funcall up1) (funcall up1) (copy-counter up1))))
(print (funcall up2))
(print (funcall up2))
(print (funcall up2))
(print (funcall up1))
"end test")
3
4
5
3
"end test"
解决此问题的最简单方法是 make-up-counter
取一个数字进行计数。
(defun make-up-counter (&optional (initial-count 0))
(let ((count initial-count))
#'(lambda () (incf count))))
但这并不能解决问题,因为我们无法检查计数器中的值,因此如果我们尝试使用计数器来为值设定种子,我们将修改当前计数器。
(defun copy-counter (counter)
(make-up-counter (funcall counter)))
为了提供一种获取值的方法,我们使闭包采用 'operation' 参数,因此我们可以检查或增加值
(defun make-up-counter (&optional (initial-count 0))
(let ((count initial-count))
#'(lambda (&optional (operation :increment))
(ecase operation
(:inspect count)
(:increment (incf count))))))
(defun copy-counter (counter)
(make-up-counter (funcall counter :inspect)))
现在当我们运行下面的代码
(let ((1st-counter (make-up-counter)))
(loop :repeat 3 :do (funcall 1st-counter))
(let ((2nd-counter (copy-counter 1st-counter)))
(loop :repeat 3 :do (funcall 1st-counter))
(format t "1st counter: ~A~%2nd counter: ~A~%"
(funcall 1st-counter :inspect)
(funcall 2nd-counter :inspect))))
它打印
1st counter: 6
2nd counter: 3
没有真正回答问题。但这样一来,副本会更容易...
(defun make-up-counter ()
(let ((n 0))
#'(lambda () (incf n))))
一般来说,我会避免将此类代码的更复杂版本用于可维护软件的生产用途。调试和自省更难。这是过去的基本 FP 知识(使用闭包来隐藏可变状态,例如参见早期的 Scheme 论文),但对于任何更复杂的事情来说,这都是一种痛苦。它隐藏了值——这很有用——但同时也使得调试变得困难。最小值是 debugger/inspector 能够查看闭包绑定。方便,因为好写,但是后面要付出代价
问题:
CL-USER 36 > (make-up-counter)
#<anonymous interpreted function 40600015BC>
这是什么?它的功能与其他所有功能一样。它没有说明它的目的、它的参数、文档、来源,它没有文档化的界面,没有有用的打印表示,代码在使用时不能更新,......我们可以向它添加更多功能 - 在内部 -但是我们可以从像 CLOS 这样的对象系统中免费获得所有这些。
(defclass counter ()
((value :initarg :start :initform 0 :type integer)))
(defmethod next-value ((c counter))
(with-slots (value) c
(prog1 value
(incf value))))
(defmethod copy-counter ((c counter))
...)
(defmethod reset-counter ((c counter))
...)
...
然后:
CL-USER 44 > (let ((c (make-instance 'counter :start 10)))
(list (next-value c)
(next-value c)
(next-value c)
c))
(10 11 12 #<COUNTER 40200E6F3B>)
CL-USER 45 > (describe (fourth *))
#<COUNTER 40200E6F3B> is a COUNTER
VALUE 13
defclass
,但是对于一个简单的计数器,您可以简单地写:
(defstruct counter (value 0))
这定义了您需要的所有内容:(make-counter)
、(make-counter :value x)
和 (copy-counter c)
都按预期工作。您的对象也可以打印成可读的。
(let* ((c (make-counter))
(d (copy-counter c)))
(incf (counter-value c))
(values c d))
#S(counter :value 1)
#S(counter :value 0)
您仍然应该导出更高级别的函数,例如 reset 和 next 这样您的计数器的用户就不需要知道它是如何实现的已实施。
我们可以做的是定义一个 API 来构造一个计数器,这样:
(make-counter <integer>) --> yields new counter starting at <integer>
(make-counter <counter>) --> yields a clone of counter
诀窍在于我们还为计数函数(计数器本身)提供了一个可选参数。如果该参数是 nil
,那么它才算数。否则它会克隆自己。这只是基本的 OOP:计数器是一个对象,它有两个方法。当 make-counter
API 被要求克隆计数器时,它委托给副本 "method".
(defun make-counter (&optional (constructor-arg 0))
(etypecase constructor-arg
(integer
(lambda (&optional opcode) ;; opcode selects method
(ecase opcode ;; method dispatch
((nil) (prog1 constructor-arg (incf constructor-arg)))
(copy-self (make-counter constructor-arg)))))
(function
(funcall constructor-arg 'copy-self))))
测试运行:
[1]> (defvar x (make-counter 3)) X [2]> (funcall x) 3 [3]> (funcall x) 4 [4]> (defvar y (make-counter x)) Y [5]> (funcall x) 5 [6]> (funcall x) 6 [7]> (funcall x) 7 [8]> (funcall y) 5 [9]> (funcall y) 6 [10]> (funcall x) 8 [11]> (funcall y) 7
与其以这种方式重载 make-counter
构造函数,不如将其拆分为两个函数 API:
(defun make-counter (&optional (constructor-arg 0))
(lambda (&optional opcode)
(ecase opcode
((nil) (prog1 constructor-arg (incf constructor-arg)))
(copy-self (make-counter constructor-arg)))))
(defun copy-counter (counter)
(funcall constructor-arg 'copy-self))
copy-counter
只不过是对象上基于操作码的低级调度 API 的包装器。