不确定这是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快捷方式加载,直到我退出正在运行的程序然后再次启动它们之后它们也没有效果。
我想要做的是不可能还是要求代码中的另一步?
我意识到这是一个微不足道的例子,但在更复杂的情况下,我可能希望这样做。
答案 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本身就是一个很好的例子。更改函数的定义(可能不是像car
或self-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可以用作程序状态和行为的方便窗口。