假设我对运行我的 Common Lisp 代码的进程有一个 REPL。它可能正在运行 SWANK/SLIME。
我想在我的实时进程中更新一个用defun定义的函数。该函数可能在 let 绑定中捕获了一些变量。本质上,这个函数是一个闭包。
如何更新该闭包中的代码而不丢失它捕获的数据?
2019-11-03:我在下面选择了一个答案,但我建议阅读所有答案。每个人都有一个有趣的见解。
假设我对运行我的 Common Lisp 代码的进程有一个 REPL。它可能正在运行 SWANK/SLIME。
我想在我的实时进程中更新一个用defun定义的函数。该函数可能在 let 绑定中捕获了一些变量。本质上,这个函数是一个闭包。
如何更新该闭包中的代码而不丢失它捕获的数据?
2019-11-03:我在下面选择了一个答案,但我建议阅读所有答案。每个人都有一个有趣的见解。
你不能从外面。
您可以尝试在相同的词法范围内为其提供帮助功能。这可能需要在其中创建一个临时功能注册表。
另一种方法是使用动态变量,但这当然只是打破了闭包。
也许相关(来自https://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html):
尊贵的 Qc Na 大师与他的学生 Anton 同行。安东希望能引起大师的讨论,道:“大师,我听说物件是一个很好的东西,这是真的吗?” Qc Na 怜悯地看着他的学生,回答说:“愚蠢的学生 - 对象只是一个穷人的闭包。”
受到责备后,安东离开了他的主人,回到了他的牢房,一心研究闭包。他仔细阅读了整个“Lambda: The Ultimate...”系列论文及其表亲,并实现了一个带有基于闭包的对象系统的小型 Scheme 解释器。他学到了很多,并期待着向他的主人通报他的进步。
在与 Qc Na 的下一次散步中,Anton 试图打动他的师父,他说:“师父,我已经认真研究了这件事,现在明白了物体确实是穷人的闭包。” Qc Na 用棍子击打 Anton 回应说:“你什么时候才能学会?闭包是穷人的对象。” 那一刻,安东顿悟了。
基本上你不能,这是避免在 LET 中使用 DEFUN 的原因之一。
可以创建一个新的闭包并尝试将状态从旧闭包复制到新闭包中。
一个问题是可移植的 Common Lisp 不允许对闭包进行太多动态访问。一个人不能“进入”一个闭包并从外部添加一些东西或替换一些东西。没有为闭包定义反射或内省操作。
因此,您以后想要对闭包做的所有事情都需要已经存在于闭包生成代码中。
假设您有以下代码:
(let ((foo 1))
(defun add (n)
n))
现在我们认为这add
是错误的,实际上应该添加一些东西吗?
我们想要这样的效果:
(let ((foo 1))
(defun add (n)
(+ n foo)))
我们如何修改原件?我们基本做不到。
如果我们有:
(let ((foo 1))
(defun get-foo ()
foo)
(defun add (n)
n))
我们可以这样做:
(let ((ff (symbol-function 'get-foo))
(fa (symbol-function 'add)))
(setf (symbol-function 'add)
(lambda (n)
(+ (funcall fa n) (funcall ff)))))
然后,这定义了一个新函数add
,该函数可以通过旧函数访问闭包值 - 在它自己的闭包中捕获。
风格
不要使用 LET 封闭的 DEFUN:
其他答案解释说你不能做你所追求的:这里有一些你不能做的实际原因。
考虑如下代码片段:
(let ((a 1) (b 3) (c 2))
(lambda (x)
(+ (* a x x) (* b x) c)))
让我们想象一下它正在被编译:一个合理的编译器会做什么?好吧,很明显它可以把它变成这样:
(lambda (x)
(+ (* 1 x x) (* 3 x) 2)
(然后可能进入
(lambda (x)
(+ (* x x) (* 3 x) 2))
也许更进一步
(lambda (x)
(+ (* x (+ x 3)) 2))
) 在最终编译之前。
函数体的这些转换都没有引用闭包引入的任何词法绑定。 整个环境已被编译掉。
因此,如果我现在想在那个环境中替换该功能,但在其他环境中,那么,已经没有环境了:我不能。
好吧,您可能会争辩说这是一个过于简单的情况,因为该函数不会改变其任何封闭变量。好吧,考虑这样的事情:
(defun make-box (contents)
(values
(lambda ()
contents)
(lambda (new)
(setf contents new))))
make-box
返回两个函数:reader 和 writer,它们都关闭了盒子的内容。而且这种共享状态不能完全编译掉。
但是完全没有理由为什么关闭状态,例如,仍然知道被关闭的变量是在编译contents
之后make-box
(甚至之前)调用的。例如,这两个函数可能都引用了某个词法状态向量,并且都知道contents
源中的东西是该向量的第一个元素。所有的名称都消失了,因此无法将共享此状态的函数之一替换为其他函数。
此外,在具有不同编译器和解释器的实现中,解释器完全没有理由与编译器共享封闭词法状态的公共表示(并且编译器和解释器都可能有几种不同的表示)。事实上 CL 规范解决了这个问题 - compile的条目说:
如果要编译的函数周围的词法环境包含除宏、符号宏或声明之外的任何绑定,则后果是不确定的。
并且这个警告是为了处理这样的情况:如果你有一堆函数共享一些词法状态,那么如果编译器的词法状态表示与解释器不同,那么只编译其中一个是有问题的。(我在 1989 年左右尝试在 D 机器上用共享词汇状态做一些可怕的事情时发现了这一点,委员会的某个好心的成员向我解释了我的困惑。)
上面的例子应该让你相信,用任何简单的方式替换与其他函数共享词法状态的函数是不可能的。但是,好吧,“不可能以任何简单的方式”与“不可能”不同。例如,语言的规范可以简单地说这应该是可能的,并且需要实现以某种方式使其工作:
这两种情况实际上都在说明该语言的实现要么需要接受相当低的性能,要么需要高超的技术,并且在任何一种情况下,都需要实现更多的东西。嗯,Common Lisp 努力的目标之一是,虽然允许使用英雄技术,但它们不应该是高性能所必需的。此外,已经感觉到该语言已经足够大了。最后,实现者几乎肯定会简单地拒绝这样的建议:他们已经有足够的工作要做,并且不想再做更多了,尤其是在这种“你需要完全重新设计编译器才能做到这一点”的级别。
所以这就是为什么,务实地,你所追求的东西是不可能的。