如何在保留捕获的变量的同时更改 lambda 中的代码?

How do I change the code in a lambda while keeping the captured vars?

假设我有一个进程 运行 我的 Common Lisp 代码的 REPL。可以是 运行 SWANK/SLIME.

我想在我的实时进程中更新一个用 defun 定义的函数。该函数可能已经捕获了 let 绑定中的一些变量。本质上,这个函数是一个闭包。

我如何更新闭包中的代码而不丢失它捕获的数据?

2019-11-03:我在下面选择了一个答案,但我建议阅读所有答案。每个人都有一个有趣的见解。

你不能从外面。

您可以尝试在相同的词法范围内为其提供辅助功能。这可能需要在其中创建一个临时功能注册表。

另一种方法是使用动态变量,当然这只会破坏闭包。

可能相关(来自 https://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html):

The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.

基本上你不能,这是避免在 LET 中使用 DEFUN 的原因之一。

可以创建一个新闭包并尝试将状态从旧闭包复制到新闭包中。

一个问题是可移植的 Common Lisp 不允许对闭包进行大量动态访问。不能 'go' 进入闭包并从外部添加或替换某些东西。没有为闭包定义反射或内省操作。

因此,您以后想要对闭包执行的所有操作都需要已经存在于闭包生成代码中。

假设您有以下代码:

(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:

  • 它们很难修改
  • 编译器不会将 DEFUN 视为顶级形式
  • 效果难以控制
  • 多次加载代码我们想做什么?
  • 它们很难调试
  • Common Lisp 有处理全局状态的方法。

其他答案解释说你不能做你想做的事:这里有一些实际原因为什么你不能。

考虑这样的代码片段:

(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))

) 最后编译之前。

None 函数体的这些转换引用闭包引入的任何词法绑定。 整个环境已被编译掉。

因此,如果我现在想在那个环境中替换那个功能,那么换一个其他的,好吧,没有环境了:我不能。

好吧,您可能会争辩说这是一个过于简单的情况,因为该函数不会改变其任何封闭变量。好吧,考虑这样的事情:

(defun make-box (contents)
  (values
   (lambda ()
     contents)
   (lambda (new)
     (setf contents new))))

make-box returns 两个函数:一个reader和一个writer,它们都关闭了盒子的内容。而且这个共享状态不能完全编译掉。

但是完全没有理由为什么关闭状态,例如,仍然知道被关闭的变量在 make-box 编译之后(甚至之前)被调用 contents ).例如,这两个函数可能都引用了一些词法状态向量,并且都知道源代码中的 contents 是该向量的第一个元素。所有名称都消失了,因此无法用其他函数替换共享此状态的函数之一。

此外,在具有不同编译器和解释器的实现中,完全没有理由解释器应该与编译器共享封闭词法状态的通用表示(并且编译器和解释器可能有几种不同的表示)。事实上,CL 规范解决了这个问题——compile 的条目说:

The consequences are undefined if the lexical environment surrounding the function to be compiled contains any bindings other than those for macros, symbol macros, or declarations.

这个警告是为了处理这样的情况:如果你有一堆共享一些词法状态的函数,那么如果编译器的词法状态表示与解释器不同,那么只编译其中一个是有问题的。 (我在 1989 年左右尝试在 D 机上使用共享词法状态做一些可怕的事情时发现了这一点,委员会的一些好心的成员向我解释了我的困惑。)


以上示例应该让您相信,用任何简单的方法替换与其他函数共享词法状态的函数是不可能的。但是,好吧,'not possible in any simple way' 与 'not possible' 不同。例如,语言的规范可以简单地说这应该是可能的,并且需要实现以某种方式使其工作:

  • 对于第一个示例,实现可能会选择不将环境编译掉从而降低性能,或者可能会选择将其编译掉但保留它的记录以便它可以围绕某些新功能恢复,可能会再次被编译掉;
  • 对于后面的示例,实现可能会选择在所有情况下都使用封闭环境的简单、低性能表示,或者使用高性能表示,但会记录环境的外观,因此可以将替换函数插入其中;
  • 无论如何,语言都需要新的功能来支持这种重新定义,这需要仔细指定,并且会使语言变得更大更复杂。

这两种情况实际上都在说明该语言的实现要么需要接受相当低的性能,要么需要高超的技术,在任何一种情况下,要实现的东西都会比已有的多。好吧,Common Lisp 努力的目标之一是,虽然英雄技术是允许的,但它们不应该 需要 以获得高性能。此外,该语言已经被认为足够大。最后,实施者几乎肯定会简单地拒绝这样的建议:他们已经有足够的工作要做,不想再做了,尤其是在这种 'you will need to completely reengineer the compiler to do this' 级别。

所以这就是为什么,从实用的角度来说,你所追求的是不可能的。