在程序运行时更改程序

时间:2013-11-28 07:11:21

标签: emacs lisp common-lisp read-eval-print-loop slime

不确定这是emacs-SLIME问题还是CL问题或SBCL问题。

我听说它说Lisp的交互性允许在程序运行时更改程序。我不知道具体是什么意思,我尝试了以下内容,将其放在一个单独的文件中:

(defparameter repl-test-var 5)
(defun repl-test ()
  (format t "repl-test-var is: ~a" repl-test-var)
  (fresh-line)
  (when (not (equal (read-line) "quit"))
    (repl-test)))

然后我编译并运行(repl-test),每次按回车键,我都会看到号码5

如果没有在REPL中输入quit,我会返回到我的文件并将5更改为6并再次编译。返回REPL,按Enter仍显示5。如果我输入quit然后再次运行(repl-test),现在我会看到6

我还尝试加载以及编译的组合,然后使用SLIME快捷方式加载,直到我退出正在运行的程序然后再次启动它们之后它们也没有效果。

我想要做的是不可能还是要求代码中的另一步?

我意识到这是一个微不足道的例子,但在更复杂的情况下,我可能希望这样做。

4 个答案:

答案 0 :(得分:2)

当在Lisp中替换函数时,它并不意味着“自修改代码”。

如果在Lisp中执行函数,则指令指针保存对函数的引用,因此该函数仍然是垃圾回收器无法回收的活动对象。

重新定义函数时,这意味着新的函数对象与名称相关联。当按名称调用该函数时,将使用新函数。

但是,对正在进行的函数的现有调用将继续使用旧函数(不再附加到符号)。当最后一个线程停止执行该函数时,它将变为垃圾。

这与Unix中的“最后关闭”非常类似,它是从目录结构中删除的打开文件。

问题不仅出现在多个线程上,而且还带有简单的递归。如果正在执行本身的函数触发重新定义,则该函数将继续使用旧主体。此外,Lisp允许递归函数中的自调用以避免通过名称绑定,但使用直接机制。如果递归函数重新定义了自己,那么仍然在同一个调用中进行的递归调用可能会继续进行同一个体。

更一般地说,Common Lisp允许编译器在同一文件中的函数之间生成有效的调用。因此,您通常必须将运行代码的替换视为模块级而不是单个功能级。如果函数A和B在同一个模块中,并且A调用B,那么如果你只是替换B而不替换A,A可以继续调用旧B(因为B被内联到A中,或者因为A没有通过符号,但为B)使用更直接的地址。您可以声明函数notinline以禁止此操作。

CL具有许多功能,可以对编译和加载的语义进行大量控制,因此您可以在应用程序中获得所需的精确行为。

答案 1 :(得分:1)

Emacs本身就是一个很好的例子。更改函数的定义(可能不是像carself-insert-command那样重要的东西!:-)并观察其行为的变化。另请参见Emacs的advice工具。

根据定义,编译后的Lisp程序没有运行交互式REPL,因此不会公开这种行为。

示例代码的问题与此类似。它占用了REPL,因此没有简单的方法来改变程序运行时的环境。

是什么让Lisp如此多才多艺(尽管不是唯一的)是(a)它给你eval; (b)在它之上编写自己的REPL非常容易; (c)最好的还提供修改和扩展其内置REPL的文档和/或钩子。

一个更有用的示例程序会eval一些输入(键盘输入?磁盘文件?经过身份验证的下载?),同时继续运行。

除了eval之外的“秘密酱”之外,很容易找到允许你在程序继续运行编译代码时升级插件的程序示例,但Lisp确实如此没有为此提供任何特殊设施 - 需要建立该计划以支持这一点。

答案 2 :(得分:1)

为了更新正在运行的程序,您必须找到一种在程序运行时与正在运行的lisp映像进行交互的方法。这可以使用多线程或调用调试器来完成。

使用多线程:尝试启动这样的功能:

(defparameter *thread* (sb-thread:make-thread #'repl-test))

如果使用emacs + slime:在*inferior-lisp*缓冲区中测试该函数,并在*slime-repl sbcl*缓冲区中更改它。

另一个演示更改正在运行的程序的测试程序是:

(defun update (i) 
   (+ i 1))

(defun hotpatch-test ()
       (loop for i = 0 then (update i) do
       (format t "~&i = ~d~%" i)
       (fresh-line)
       (sleep 5)))

开头
(defparameter *thread* (sb-thread:make-thread #'hotpatch-test))

观察正在打印的数字,然后更改update 的定义,例如

(defun update (i)
   (+ i 2))

并查看数字序列输出如何变化。

最后,线程可能会被

杀死
(sb-thread:terminate-thread *thread*)

<强>更新

在不使用多处理的情况下更新正在运行的程序的另一种方法是使用C-c(或者C-c C-c在软件中)中断程序,在调试器中加载/输入新代码,然后选择continue重新启动,以便从中断的地方继续运行程序。

答案 3 :(得分:1)

由于您的SBCL图像是单线程的,因此在REPL繁忙时,您的代码似乎没有被重新加载。您可以通过检查:* features *中是否存在sb-thread来确定您的SBCL是单线程的。当SBCL本身被编译时,确定了线程与非线程,因此为了获得你想要的行为,你需要获得一个启用了线程的SBCL二进制文件,或者在启用线程的情况下编译SBCL。

缺少线程会妨碍交互式开发的一些好处(如在测试中,或者如果您想开发一个运行在同一图像中的服务器组件的Web程序),但它仍然带来一些好处打开。交互式开发的一些方便的方面,不要求你的程序主动“做”任何让你享受它们,包括你只需要重新加载你改变的程序部分,这个重新加载不会导致程序转储它已加载的数据(作为重启可能),并且REPL可以用作程序状态和行为的方便窗口。