【问题标题】:Is Lisp the only language with REPL?Lisp 是唯一带有 REPL 的语言吗?
【发布时间】:2011-08-05 23:39:59
【问题描述】:

除了 Lisp(ruby、scala)之外,还有其他语言说他们使用 REPL(Read、Eval、Print、Loop),但不清楚 REPL 的含义是否与 Lisp 中的相同。 Lisp REPL 与非 Lisp REPL 有何不同?

【问题讨论】:

  • 在下面的 cmets 中出现的问题是 “哪些论据用于声称 LISP REPL 是唯一的真正的 REPL?”。这个问题是主观的和争论的。
  • 在 Lisp 中,我们不必在代码更改后重新启动 REPL。我们可以编译一个函数并立即试用。我们不会失去国家。对象在类定义更改后甚至(懒惰地)更新。 Common Lisp 是一种基于图像的语言。

标签: lisp programming-languages read-eval-print-loop


【解决方案1】:

我想你可以说 Scala 的“REPL”是一个“RCRPL”:读取、编译、运行、打印。但由于编译器在内存中保持“热”状态,因此对于正在进行的交互来说非常快——只需几秒钟即可启动。

【讨论】:

  • 我知道 Clojure(一种 Lisp 语言)在内存中也有编译器,所以我不明白你的意思。我很抱歉。
  • 我认为您的问题更多是关于“E”的含义,即“评估”,通常是动态/脚本语言的一个特性。但是在现代计算机上,即使像 Scala 这样大的编译器也可以交互运行。无论哪种方式,含义都差不多:用户输入声明和表达式,立即获得结果,并且可以轻松地在后续表达式中重用以前的结果。
  • 哦,是的,我现在明白你的意思了。我需要咀嚼一段时间。 :)
  • 不,它仍然应该被认为是一个“REPL”——“E”是如何实现的无关紧要......事实上,许多 Lisp 实现就是这样做的:通过编译和运行输入来评估表达。
【解决方案2】:

鉴于 REPL 的概念只是读取、评估、打印和循环,因此有许多语言的 REPL 也就不足为奇了:

C/C++

C#/LINQ

Erlang

Haskell(在 Windows 上)

Java

Javascript

Julia

Perl

Python

Ruby

Scala

Smalltalk -- 我是在 REPL 上学到的!

【讨论】:

  • 很多人会争辩说 Lisp 只是 REPL 的语言,所以我想我的意思是问他们为什么这么认为。但也许他们只是这么说的 Lisp 程序员。 ;)
  • 我也将 erlang 添加到列表中,尽管它不能完全访问该语言可以做的所有事情。此外,shell 语言通常在某处有一个 REPL :) 只需执行 sh 即可查看。 :)
  • 随意添加到列表中;我显然忘记了一些!
  • GHCi 适用于 GHC 支持的任何平台。 WinGHCi 只是它的一个 gui 前端。
  • Eli:我从没听过有人这么说。你应该问他们。
【解决方案3】:

许多人认为 REPL 的行为必须与 LISP 中的行为完全相同,否则它就不是真正的 REPL。相反,他们认为它有些不同,例如 CLI(命令行解释器)。老实说,我倾向于认为,如果它遵循以下基本流程:

  • 读取用户输入
  • 评估输入
  • 打印输出
  • 循环回读

那么它就是一个 REPL。如前所述,有很多语言具有上述功能。

有关此类讨论的示例,请参见 this reddit thread

【讨论】:

    【解决方案4】:

    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/... 交互界面都一样。

    问题:

    那么,阅读表达式、评估表达式并打印其值的最初想法有多重要?这与其他语言所做的事情相比是否重要:阅读文本、解析文本、执行文本、可选地打印某些内容以及可选地打印返回值?通常没有真正使用返回值。

    所以有两种可能的答案

    1. Lisp REPL 与大多数其他文本交互界面不同,因为它基于 s 表达式的数据 I/O 并评估这些概念。

    2. REPL 是一个通用术语,用于描述与编程语言实现或其子集的文本交互接口。

    Lisp 中的 REPL

    在实际实现中,Lisp REPL 具有复杂的实现并提供大量服务,直至输入和输出对象的可点击表示(符号、CLIM、SLIME)。高级 REPL 实现例如在 SLIME(一个流行的基于 Emacs 的 Common Lisp IDE)、McCLIMLispWorksAllegro 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))
    

    现在我们在它周围添加一些代码来将变量ab 绑定到一些数字。我们正在使用 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 ...)
    

    【讨论】:

    • 请注意,“EXPRESSIONS”不会出现在 REPL 中。它是 Lisp 实现的产物。以 Forth 为例,它的解释器是 BEGIN REFILL WHILE INTERPRET STATE @ 0= IF ." OK" CR THEN REPEAT 的变体。其中的步骤是读取、评估、打印和循环,Forth 的评估与 Lisp 的评估一样与系统的其余部分密切相关。然而,Lisp 支持“代码就是数据”和 s-exp,而 Forth 有一个“数据就是代码”的观点。
    • @Daniel C. Sobral:符号表达式不是 Lisp 实现的产物。这是它的核心概念之一。 Forth 非常接近 Lisp。还有像 Prolog 这样的其他语言。
    • @Rainer 使用符号表达式来实现 REPL 是 Lisp 实现的产物。当然,它们是 Lisp 本身的核心概念,但它们并没有使 REPL 在任何可见方面有所不同——因此,它是一个实现细节。
    • @Daniel C. Sobrak:我不了解你,但我家里有一台 Lisp 机器,它的 REPL 产生了巨大的明显差异。
    • @Dabiel C. Sobral:这就是我要说的,在 REPL 中使用 s 表达式会极大地改变与它的交互方式。
    【解决方案5】:

    有一个名为multi-repl 的不错的项目,它通过 Node.JS 公开了各种 REPL:

    https://github.com/evilhackerdude/multi-repl

    如果您查看支持的语言列表,很明显不仅 Lisp 有 REPL 的概念。

    • clj (clojure)
    • ghci (ghc)
    • ipython
    • irb(红宝石)
    • js(蜘蛛猴)
    • 节点
    • 蟒蛇
    • sbcl
    • v8

    事实上,在 Ruby 中实现一个微不足道的方法相当容易:

    repl = -> prompt { print prompt; puts(" => %s" % eval(gets.chomp!)) }
    loop { repl[">> "] }
    

    【讨论】:

    【解决方案6】:

    我认为比较两种方法很有趣。 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 而言,这根本不重要。

    让我们从外部考虑:

    1. READ -- 从标准输入返回输入
    2. EVAL -- 将所述输入作为语言中的表达式处理
    3. PRINT -- 打印 EVAL 的结果
    4. 循环——返回阅读

    如果不深入了解实现细节,就无法将 Lisp REPL 与例如 Ruby REPL 区分开来。作为函数,它们是相同的。

    【讨论】:

    • 要了解一种机制,您可以查看表面并比较函数名称,或者您可以尝试查看构造的语义。 Still Forth 非常接近 Lisp。更接近 Prolog 系统的 REPL。
    • @Rainer 查看语义,还是查看实现?函数的语义只关心同一组输入导致同一组输出。
    【解决方案7】:

    Lisp REPL 与非 Lisp REPL 有何不同?

    让我们比较一下Common Lisp 的 REPL 和 Python 的 IPython。

    主要有两点:

    • Lisp 是一种基于图像的语言。更改后无需重新启动进程/REPL/整个应用程序。我们逐个函数编译代码(带有编译器警告等)。
    • 我们不松动状态。更重要的是,当我们更新类定义时,REPL 中的对象也会更新,遵循我们可以控制的规则。这样我们就可以在正在运行的系统中热重载代码。

    在 Python 中,通常是启动 IPython 或进入 ipdb。在您尝试新功能之前,您需要定义一些数据。你编辑了你的源代码,你想再试一次,所以你退出了 IPython 并重新开始了整个过程。在 Lisp(主要是 Common Lisp)中,完全没有,它更具交互性。

    【讨论】:

    • 我们可以说 CL 就像汇编程序,而 REPL 相当于过去的调试提示符吗?