我听过 Ron Garret 的 Google 演讲 ( http://www.youtube.com/watch?v=_gZK0tW8EhQ ) 并阅读了论文 ( http://www.flownet.com/gat/jpl-lisp.html ) ,但我不明白它是如何使用 REPL 来“纠正”据称正在运行的代码的。DS-1 运行的 Lisp 代码是某种虚拟机吗?还是它“活”在 REPL 的现实世界中?或者 Lisp 代码是被替换的可执行文件?当您通过 REPL 动态更改正在运行的 Lisp 代码时,究竟发生了什么?
1 回答
尽管大多数程序都是作为只包含运行程序所需组件的可执行文件构建和分发的,但 Lisp 可以作为映像分发,其中不仅包含特定程序的组件,还包含大部分或全部 Lisp 运行时和开发环境。
REPL 是提供对运行中的 Lisp 环境的交互式访问的典型机制。REPL 的两个关键组件 Read 和 Eval 暴露了 Lisp 运行时系统的大部分内容。例如,今天的许多 Lisp 系统通过编译提供的表单(由 Reader 读取)、将表单编译为机器代码、然后执行结果来实现 Eval。这与解释表格相反。一些系统,尤其是在过去,既包含一个快速执行且适合交互式访问的解释器,也包含一个生成更好代码的编译器。但是现代系统足够快,编译器阶段并不明显,只是放弃了解释器。
当然,你今天也可以做非常相似的事情。一个简单的示例是在托管 PHP 的 Linux 机器上运行 SSH。您的 PHP 服务器已启动并运行并运行,为页面和请求提供服务。但是您通过 SSH 登录,检查并修复一个 PHP 文件,一旦您保存该文件,您的所有用户都会实时看到新结果——系统会自动更新。
PHP 运行在 Linux 运行时与 Lisp 运行在 Lisp 运行时这一事实是一个细节。效果是一样的。PHP 没有被编译的事实也是一个细节。例如,您可以在 Java 服务器上执行相同的操作:修改 JSP,将其保存,然后将 JSP 作为 Java 源代码转换为 Servlet,然后由 Java 运行时动态编译,然后加载到执行容器,替换旧代码。
Lisp 做到这一点的能力非常好,而且在很久以前它就非常有趣。今天,情况已经不那么好了,因为有不同的系统提供了类似的功能。
附加物:
不,Lisp 不是虚拟机,没必要那么复杂。
这个概念的关键是动态调度。对于动态调度,在调用函数之前会涉及一些查找。
在像 C 这样的静态语言中,一旦链接器和加载器完成对可执行文件的处理以准备开始执行,事物的位置就几乎是一成不变的。
所以,在 C 中,如果你有一些简单的东西,比如:
int add(int i) {
return i + 1;
}
void main() {
add(1);
}
在编译链接和加载程序之后,add
函数的地址将一成不变,因此引用该函数的东西将确切地知道在哪里找到它。
所以,在汇编语言中:(注意这是一种人为的汇编语言)
add: pop r1 ; pop R1 from the stack, loading the i parameter
add r1, 1; Add 1 to the parameter.
push r1 ; push result of function call
rts ; return from subroutine
main: push 1 ; Push parameter to function
call add ; call function
pop r1 ; gather (and ignore) the result
所以,你可以看到这里add
是固定的。
在像 Lisp 这样的东西中,函数是间接引用的。
int add(int i) {
return i + 1;
}
int *add_ptr() = &add;
void main() {
*(add_ptr)(1);
}
在汇编中你得到:
add: pop r1 ; pop R1 from the stack, loading the i parameter
add r1, 1; Add 1 to the parameter.
push r1 ; push result of function call
rts ; return from subroutine
add_ptr: dw add ; put the address of the add routine in add_ptr
main: push 1 ; Push parameter to function
mov r1, add_ptr ; Put the contents of add_ptr into R1
call (r1) ; call function indirectly through R1
pop r1 ; gather (and ignore) the result
现在,您可以在这里看到,它不是add
直接调用,而是通过add_ptr
. 在 Lisp 运行时中,它具有编译新代码的能力,当这种情况发生时,它add_ptr
会被覆盖以指向新编译的代码。您可以看到代码main
永远不必更改,它将调用 add_ptr 指向的任何函数。
由于 Lisp 中的大多数函数都是通过它们的符号间接引用的,所以在运行系统的“背后”会发生很多变化,并且系统将继续运行。
当一个函数被重新编译时,旧的函数代码(假设没有其他引用)变得有资格进行垃圾回收,并且通常最终会消失。
您还可以看到,当系统被垃圾回收时,任何被移动的代码(例如 add 函数的代码)都可以被运行时移动,并且它的新位置被更新,add_ptr
因此即使在代码之后系统仍将继续运行并被垃圾收集器重新定位。
因此,这一切的关键是通过某种查找机制调用您的函数。有了这个,你就有了很大的灵活性。
注意,您也可以在运行 C 系统时这样做,例如。您可以将代码放入动态库中,加载库,执行代码,如果需要,您可以构建一个新的动态库,关闭旧的,打开新的,然后调用新的代码——所有这些都在一个“运行”系统。动态库接口提供了隔离代码的查找机制。