除了 Lisp(ruby、scala)之外,还有其他语言说他们使用 REPL(Read、Eval、Print、Loop),但不清楚 REPL 的含义是否与 Lisp 中的相同。Lisp REPL 与非 Lisp REPL 有何不同?
7 回答
REPL的想法来自 Lisp 社区。还有其他形式的文本交互界面,例如命令行界面。一些文本接口还允许执行某种编程语言的子集。
REPL 代表 READ EVAL PRINT LOOP: (loop (print (eval (read))))。
上述四个函数中的每一个都是原始的 Lisp 函数。
在 Lisp 中,REPL 不是命令行解释器 (CLI)。READ
不读取命令,REPL 不执行命令。READ
以 s-expression 格式读取输入数据并将其转换为内部数据。因此该READ
函数可以读取所有类型的 s 表达式——不仅仅是 Lisp 代码。
READ 读取一个 s 表达式。这是一种也支持编码源代码的数据格式。READ 返回 Lisp 数据。
EVAL 以 Lisp 数据的形式获取 Lisp 源代码并对其进行评估。可能会发生副作用,并且 EVAL 返回一个或多个值。没有定义如何使用解释器或编译器实现 EVAL。实现使用不同的策略。
PRINT 获取 Lisp 数据并将其作为 s 表达式打印到输出流。
LOOP 只是围绕这个循环。在现实生活中,REPL 更为复杂,包括错误处理和子循环,即所谓的中断循环。如果出现错误,则在错误的上下文中获得另一个 REPL,并添加了调试命令。一次迭代中产生的值也可以用作下一次评估的输入。
由于 Lisp 同时使用代码即数据和函数元素,因此与其他编程语言略有不同。
相似的语言也将提供相似的交互界面。例如,Smalltalk 也允许交互式执行,但它不像 Lisp 那样使用 I/O 数据格式。任何 Ruby/Python/... 交互界面都一样。
问题:
那么阅读表达式、评估表达式并打印其价值的最初想法有多重要?这与其他语言所做的事情相比是否重要:阅读文本、解析文本、执行文本、可选地打印某些内容以及可选地打印返回值?通常没有真正使用返回值。
所以有两个可能的答案:
Lisp REPL 与大多数其他文本交互界面不同,因为它基于 s 表达式的数据 I/O 的想法并评估这些。
REPL 是一个通用术语,用于描述与编程语言实现或其子集的文本交互接口。
Lisp 中的 REPL
在实际实现中,Lisp REPL 具有复杂的实现并提供大量服务,直至输入和输出对象的可点击表示(Symbolics、CLIM、SLIME)。例如,高级 REPL 实现在SLIME(一种流行的基于 Emacs 的 Common Lisp IDE)、McCLIM、LispWorks和Allegro CL中可用。
Lisp REPL 交互的示例:
产品清单和价格:
CL-USER 1 > (setf *products* '((shoe (100 euro))
(shirt (20 euro))
(cap (10 euro))))
((SHOE (100 EURO)) (SHIRT (20 EURO)) (CAP (10 EURO)))
订单,产品清单和数量:
CL-USER 2 > '((3 shoe) (4 cap))
((3 SHOE) (4 CAP))
订单价格*
是一个包含最后一个 REPL 值的变量。它不包含此值作为字符串,而是真实的实际数据。
CL-USER 3 > (loop for (n product) in *
sum (* n (first (second (find product *products*
:key 'first)))))
340
但你也可以计算 Lisp 代码:
让我们看一个函数,它将两个参数的平方相加:
CL-USER 4 > '(defun foo (a b) (+ (* a a) (* b b)))
(DEFUN FOO (A B) (+ (* A A) (* B B)))
第四个元素只是算术表达式。*
指最后一个值:
CL-USER 5 > (fourth *)
(+ (* A A) (* B B))
现在我们在它周围添加一些代码来绑定变量a
和b
一些数字。我们正在使用 Lisp 函数LIST
来创建一个新列表。
CL-USER 6 > (list 'let '((a 12) (b 10)) *)
(LET ((A 12) (B 10)) (+ (* A A) (* B B)))
然后我们评估上面的表达式。再次,*
指的是最后一个值。
CL-USER 7 > (eval *)
244
每次REPL
交互都会更新几个变量。示例是*
,**
和***
用于之前的值。也有+
用于先前的输入。这些变量的值不是字符串,而是数据对象。+
将包含 REPL 读取操作的最后结果。例子:
变量的值是*print-length*
多少?
CL-USER 8 > *print-length*
NIL
让我们看看如何读取和打印列表:
CL-USER 9 > '(1 2 3 4 5)
(1 2 3 4 5)
现在让我们将上面的符号设置*print-length*
为 3。++
指的是第二个之前的输入读取,作为数据。SET
设置符号值。
CL-USER 10 > (set ++ 3)
3
然后上面的列表打印不同。**
指的是第二个先前的结果 - 数据,而不是文本。
CL-USER 11 > **
(1 2 3 ...)
我认为比较两种方法很有趣。Lisp 系统中的基本 REPL 循环如下所示:
(loop (print (eval (read))))
下面是 REPL 循环的两个实际 Forth 实现。我在这里什么都没有留下——这是这些循环的完整代码。
: DO-QUIT ( -- ) ( R: i*x -- )
EMPTYR
0 >IN CELL+ ! \ set SOURCE-ID to 0
POSTPONE [
BEGIN \ The loop starts here
REFILL \ READ from standard input
WHILE
INTERPRET \ EVALUATE what was read
STATE @ 0= IF ." OK" THEN \ PRINT
CR
REPEAT
;
: quit
sp0 @ 'tib !
blk off
[compile] [
begin
rp0 @ rp!
status
query \ READ
run \ EVALUATE
state @ not
if ." ok" then \ PRINT
again \ LOOP
;
Lisp 和 Forth 做完全不同的事情,特别是在 EVAL 部分,但在 PRINT 部分也是如此。然而,他们共享一个事实,即两种语言的程序都是通过将源代码提供给各自的循环来运行的,并且在这两种情况下,代码只是数据(尽管在 Forth 情况下,它更像是数据也是代码)。
我怀疑有人说只有 LISP 有 REPL 是 READ 循环读取由 EVAL 解析的 DATA,并且创建了一个程序,因为 CODE 也是 DATA。Lisp 和其他语言之间的区别在很多方面都很有趣,但就 REPL 而言,这根本不重要。
让我们从外部考虑:
- READ -- 从标准输入返回输入
- EVAL - 将所述输入处理为语言中的表达式
- PRINT -- 打印 EVAL 的结果
- 循环——返回阅读
如果不深入了解实现细节,就无法将 Lisp REPL 与例如 Ruby REPL 区分开来。作为功能,它们是相同的。
我想你可以说 Scala 的“REPL”是一个“RCRPL”:读取、编译、运行、打印。但是由于编译器在内存中保持“热”状态,因此对于正在进行的交互来说非常快——只需几秒钟即可启动。
有很多人认为 REPL 的行为必须与 LISP 中的完全一样,否则它就不是真正的 REPL。相反,他们认为它有些不同,例如 CLI(命令行解释器)。老实说,我倾向于认为,如果它遵循以下基本流程:
- 读取用户的输入
- 评估该输入
- 打印输出
- 循环回读
然后它是一个REPL。如前所述,有很多语言具有上述功能。
有关此类讨论的示例,请参见此 reddit 线程。
There's a nice project called multi-repl
which exposes various REPLs via Node.JS:
https://github.com/evilhackerdude/multi-repl
If you look at the list of supported languages, it's quite clear that not only Lisp has the concept of a REPL.
- clj (clojure)
- ghci (ghc)
- ipython
- irb (ruby)
- js (spidermonkey)
- node
- python
- sbcl
- v8
In fact implementing a trivial one in Ruby is fairly easy:
repl = -> prompt { print prompt; puts(" => %s" % eval(gets.chomp!)) }
loop { repl[">> "] }
Lisp REPL 与非 Lisp REPL 有何不同?
让我们比较一下Common Lisp的 REPL 和 Python 的 IPython。
主要的两点是:
- Lisp 是一种基于图像的语言。更改后无需重新启动进程/REPL/整个应用程序。我们逐个函数编译我们的代码(带有编译器警告等)。
- 我们不松散状态。更重要的是,当我们更新类定义时,REPL 中的对象也会更新,遵循我们可以控制的规则。这样我们就可以在正在运行的系统中热重载代码。
在 Python 中,通常是启动 IPython 或进入 ipdb。在尝试新功能之前,您需要定义一些数据。你编辑了你的源代码,你想再试一次,所以你退出了 IPython 并重新开始了整个过程。在 Lisp(主要是 Common Lisp)中,一点也不,它更具交互性。