【问题标题】:Can I use Common Lisp for SICP or is Scheme the only option?我可以将 Common Lisp 用于 SICP 还是 Scheme 是唯一的选择?
【发布时间】:2010-11-12 15:39:29
【问题描述】:

另外,即使我可以使用 Common Lisp,我应该吗?方案更好吗?

【问题讨论】:

  • Sussman 使用 Scheme 是因为它在下午很容易上手,而且不碍事。如果你使用像 Common Lisp 这样的语言,你首先必须学习它(可能需要很长时间),然后弄清楚如何轻松实现 Scheme 所做的事情,比如第一类函数,然后将书改编为你的语言。一直以来,您都忘记了这本书实际上要教什么!用其他语言做 SICP 是可能的,甚至很有趣,但只是作为高级程序员和语言极客的练习(他们都已经在一定程度上知道 SICP 的想法)。

标签: lisp scheme common-lisp sicp


【解决方案1】:

它们相似但不相同。

我相信如果你使用 Scheme 会更容易。

【讨论】:

    【解决方案2】:

    编辑: Nathan Sanders 的评论是正确的。自从我上次阅读这本书以来显然已经有一段时间了,但我只是检查了一下,它并没有直接使用call/cc。我赞成 Nathan 的回答。


    无论您使用什么都需要实现延续,而 SICP 经常使用这种延续。甚至不是所有的 Scheme 解释器都实现了它们,而且我不知道有任何 Common Lisp 实现了。

    【讨论】:

    • Common Lisp 可以在其上写入延续。 Weblocks 就是这样一款使用延续来编写网络应用程序的应用程序。
    • 所以,基本上我需要使用Scheme。您推荐哪种实施方式?我只尝试过 PLT Scheme。
    • 啊,我猜 cmets 中没有链接:P
    • 在上基于SICP的编程课程时,我们使用了DrScheme,似乎已经融入PLT Scheme:plt-scheme.org
    • 这是错误的:SICP 不使用 Scheme 原生延续。它要求您自己实现它们,这几乎可以用任何语言实现。 (顺便说一下,我会推荐 PLT Scheme)
    【解决方案3】:

    你已经知道一些 Common Lisp 了吗?我想这就是你所说的“Lisp”的意思。在这种情况下,您可能想使用它而不是 Scheme。如果您也不知道,并且您只是为了学习经验而通过 SICP 工作,那么使用 Scheme 可能会更好。它为新学习者提供了更好的支持,您不必从 Scheme 翻译到 Common Lisp。

    有区别;具体来说,SICP 的高功能风格在 Common Lisp 中更加冗长,因为您必须在传递函数时引用它们并使用 funcall 调用绑定到变量的函数。

    但是,如果你想使用 Common Lisp,你可以尝试在标签 SICP 下使用 Eli Bendersky's Common Lisp translations of the SICP code

    【讨论】:

      【解决方案4】:

      将 SICP 与 Common Lisp 结合使用是可行且有趣的

      您可以使用 Common Lisp 来学习 SICP,而不会遇到太多问题。本书中使用的 Scheme 子集不是很复杂。 SICP 不使用宏,也不使用延续。有DELAY和FORCE,用Common Lisp写几行就可以了。

      对于初学者来说,使用(function foo)(funcall foo 1 2 3) 实际上更好(恕我直言!),因为在学习函数式编程部分时代码会变得更清晰。您可以看到变量和 lambda 函数在哪里被调用/传递。

      Common Lisp 中的尾调用优化

      使用 Common Lisp 有一个很大的缺点:尾调用优化 (TCO)。 Common Lisp 在其标准中不支持 TCO(因为与其他语言的交互不清楚,并非所有计算机体系结构都直接支持它(想想 JVM),并非所有编译器都支持它(一些 Lisp 机器),它进行了一些调试/跟踪/更加努力,...)。

      有三种方法可以解决这个问题:

      1. 希望堆栈不会爆裂。不好。
      2. 使用支持 TCO 的 Common Lisp 实现。有一些。见下文。
      3. 使用 DOTIMES、DO、LOOP、...将功能循环(和类似结构)重写为循环(和类似结构)

      我个人会推荐 2 或 3 个。

      Common Lisp 具有出色且易于使用的编译器,支持 TCO(SBCL、LispWorks、Allegro CL、Clozure CL,...),并且作为开发环境使用内置编译器或 GNU Emacs/SLIME。

      对于与 SICP 一起使用,我推荐 SBCL,因为它始终默认编译,默认支持 TCO,并且编译器会发现很多编码问题(未声明的变量、错误的参数列表、一堆类型错误、. ..)。这对学习有很大帮助。通常确保代码已编译,因为 Common Lisp 解释器通常不支持 TCO。

      有时编写一两个宏并提供一些Scheme 函数名称以使代码看起来更像Scheme 也可能会有所帮助。例如,您可以在 Common Lisp 中使用 DEFINE 宏。

      对于更高级的用户,有一个用 Common Lisp 编写的旧 Scheme 实现(称为 Pseudo Scheme),它应该运行 SICP 中的大部分代码。

      我的建议:如果你想加倍努力并使用 Common Lisp,那就去做吧。

      为了更容易理解必要的更改,我添加了一些示例 - 请记住,它需要一个支持 尾调用优化的 Common Lisp 编译器:

      示例

      让我们看看这个来自 SICP 的简单代码:

      (define (factorial n)
        (fact-iter 1 1 n))
      
      (define (fact-iter product counter max-count)
        (if (> counter max-count)
            product
            (fact-iter (* counter product)
                       (+ counter 1)
                       max-count)))
      

      我们可以通过 DEFINE 宏直接在 Common Lisp 中使用它:

      (defmacro define ((name &rest args) &body body)
        `(defun ,name ,args ,@body))
      

      现在您应该使用 SBCL、CCL、Allegro CL 或 LispWorks。这些编译器默认支持 TCO。

      让我们使用 SBCL:

      * (define (factorial n)
          (fact-iter 1 1 n))
      ; in: DEFINE (FACTORIAL N)
      ;     (FACT-ITER 1 1 N)
      ; 
      ; caught STYLE-WARNING:
      ;   undefined function: FACT-ITER
      ; 
      ; compilation unit finished
      ;   Undefined function:
      ;     FACT-ITER
      ;   caught 1 STYLE-WARNING condition
      
      FACTORIAL
      * (define (fact-iter product counter max-count)
          (if (> counter max-count)
              product
              (fact-iter (* counter product)
                         (+ counter 1)
                         max-count)))
      
      FACT-ITER
      * (factorial 1000)
      
      40238726007709....
      

      另一个例子:符号微分

      SICP 有一个用于微分的 Scheme 示例:

      (define (deriv exp var)
        (cond ((number? exp) 0)
              ((variable? exp)
               (if (same-variable? exp var) 1 0))
              ((sum? exp)
               (make-sum (deriv (addend exp) var)
                         (deriv (augend exp) var)))
              ((product? exp)
               (make-sum
                 (make-product (multiplier exp)
                               (deriv (multiplicand exp) var))
                 (make-product (deriv (multiplier exp) var)
                               (multiplicand exp))))
              (else
               (error "unknown expression type -- DERIV" exp))))
      

      让这段代码在 Common Lisp 中运行很容易:

      • 有些函数有不同的名字,number? 在 CL 中是 numberp
      • CL:COND 使用 T 而不是 else
      • CL:ERROR 使用 CL 格式字符串

      让我们为一些函数定义方案名称。通用 Lisp 代码:

      (loop for (scheme-symbol fn) in
            '((number?      numberp)
              (symbol?      symbolp)
              (pair?        consp)
              (eq?          eq)
              (display-line print))
            do (setf (symbol-function scheme-symbol)
                     (symbol-function fn)))
      

      我们上面的define 宏:

      (defmacro define ((name &rest args) &body body)
        `(defun ,name ,args ,@body))
      

      Common Lisp 代码:

      (define (variable? x) (symbol? x))
      
      (define (same-variable? v1 v2)
        (and (variable? v1) (variable? v2) (eq? v1 v2)))
      
      (define (make-sum a1 a2) (list '+ a1 a2))
      
      (define (make-product m1 m2) (list '* m1 m2))
      
      (define (sum? x)
        (and (pair? x) (eq? (car x) '+)))
      
      (define (addend s) (cadr s))
      
      (define (augend s) (caddr s))
      
      (define (product? x)
        (and (pair? x) (eq? (car x) '*)))
      
      (define (multiplier p) (cadr p))
      
      (define (multiplicand p) (caddr p))
      
      (define (deriv exp var)
        (cond ((number? exp) 0)
              ((variable? exp)
               (if (same-variable? exp var) 1 0))
              ((sum? exp)
               (make-sum (deriv (addend exp) var)
                         (deriv (augend exp) var)))
              ((product? exp)
               (make-sum
                 (make-product (multiplier exp)
                               (deriv (multiplicand exp) var))
                 (make-product (deriv (multiplier exp) var)
                               (multiplicand exp))))
              (t
               (error "unknown expression type -- DERIV: ~a" exp))))
      

      让我们在 LispWorks 中尝试一下:

      CL-USER 19 > (deriv '(* (* x y) (+ x 3)) 'x)
      (+ (* (* X Y) (+ 1 0)) (* (+ (* X 0) (* 1 Y)) (+ X 3)))
      

      Common Lisp 中来自 SICP 的流示例

      请参阅 SICP 中的book code in chapter 3.5。我们使用上面对 CL 的添加。

      SICP 提到了delaythe-empty-streamcons-stream,但没有实现。我们在这里提供了一个 Common Lisp 的实现:

      (defmacro delay (expression)
        `(lambda () ,expression))
      
      (defmacro cons-stream (a b)
        `(cons ,a (delay ,b)))
      
      (define (force delayed-object)
        (funcall delayed-object))
      
      (defparameter the-empty-stream (make-symbol "THE-EMPTY-STREAM"))
      

      现在来自本书的可移植代码:

      (define (stream-null? stream)
        (eq? stream the-empty-stream))
      
      (define (stream-car stream) (car stream))
      
      (define (stream-cdr stream) (force (cdr stream)))
      
      (define (stream-enumerate-interval low high)
        (if (> low high)
            the-empty-stream
          (cons-stream
           low
           (stream-enumerate-interval (+ low 1) high))))
      

      现在 Common Lisp 与 stream-for-each 不同:

      • 我们需要使用cl:progn 而不是begin
      • 函数参数需要用cl:funcall调用

      这是一个版本:

      (defmacro begin (&body body) `(progn ,@body))
      
      (define (stream-for-each proc s)
        (if (stream-null? s)
            'done
            (begin (funcall proc (stream-car s))
                   (stream-for-each proc (stream-cdr s)))))
      

      我们还需要使用cl:function传递函数:

      (define (display-stream s)
        (stream-for-each (function display-line) s))
      

      然后这个例子有效:

      CL-USER 20 > (stream-enumerate-interval 10 20)
      (10 . #<Closure 1 subfunction of STREAM-ENUMERATE-INTERVAL 40600010FC>)
      
      CL-USER 21 > (display-stream (stream-enumerate-interval 10 1000))
      
      10 
      11 
      12 
      ...
      997 
      998 
      999 
      1000 
      DONE
      

      【讨论】:

      • 非常完整和广泛的答案!谢谢雷纳!
      【解决方案5】:

      您在这里有几个答案,但没有一个是真正全面的(我不是说有足够的细节或足够长)。首先,底线是:如果您想获得良好的 SICP 体验,则应该使用 Common Lisp。

      如果你对 Common Lisp 了解不多,那么就这样吧。 (显然你可以忽略这个建议,有些人只能通过艰难的方式学习。)

      如果您已经了解 Common Lisp,那么您可能会成功,但需要付出相当大的努力,并且会对您的整体学习体验造成相当大的损害。有一些基本问题将 Common Lisp 和 Scheme 分开,这使得尝试将前者与 SICP 结合使用是一个非常糟糕的主意。事实上,如果您具备使其工作的知识水平,那么无论如何您都可能高于 SICP 水平。我并不是说这不可能——当然可以在 Common Lisp 中实现整本书(例如,参见 Bendersky 的页面),就像在 C 或 Perl 或其他任何东西中一样。与 Scheme 相距甚远的语言只会变得更加困难。 (例如,ML 可能比 Common Lisp 更容易使用,即使它的语法非常不同。)

      以下是其中一些主要问题,按重要性递增的顺序排列。 (我并不是说这份清单是详尽无遗的,我敢肯定还有一大堆额外的问题我在这里省略了。)

      1. NIL 和相关问题,以及不同的名称。

      2. 动态范围。

      3. 尾调用优化。

      4. 函数和值的独立命名空间。

      我现在将在以下各点上进行扩展:

      第一点是最技术性的。在 Common Lisp 中,NIL 既用作空列表又用作 false 值。就其本身而言,这并不是什么大问题,事实上 SICP 的第一版也有类似的假设——空列表和 false 是相同的值。但是,Common Lisp 的NIL 仍然不同:它也是一个符号。因此,在 Scheme 中,您有一个明确的区分:某物要么是列表,要么是值的原始类型之一——但在 Common Lisp 中,NIL 不仅是假的,而且是空列表:它也是 also 一个符号。除此之外,你会得到许多稍微不同的行为——例如,在 Common Lisp 中,空列表的头部和尾部(carcdr)本身就是空列表,而在 Scheme 中你如果您尝试这样做,将会出现运行时错误。最重要的是,您有不同的名称和命名约定,例如——Common Lisp 中的谓词按约定以 P 结尾(例如,listp),而 Scheme 中的谓词以问号结尾(例如,list? ); Common Lisp 中的mutators 没有特定的约定(有些有N 前缀),而在Scheme 中它们几乎总是有! 的后缀。此外,Common Lisp 中的普通赋值通常setf,它也可以对组合进行操作(例如,(setf (car foo) 1)),而在Scheme 中它是set!,并且仅限于设置绑定变量。 (请注意,Common Lisp 也有限制版本,称为setq。但几乎没有人使用它。)

      第二点是更深层次的一点,可能会导致您的代码行为完全无法理解。问题是在 Common Lisp 中,函数参数是词法范围的,但使用 defvar 声明的变量是动态 范围的。有很多依赖于词法范围绑定的解决方案——而在 Common Lisp 中它们根本行不通。当然,Common Lisp 具有 词法范围这一事实意味着您可以通过非常小心新的绑定来解决这个问题,并且可能使用宏来绕过默认的动态范围——但同样,这需要比典型新手更广泛的知识。事情变得更糟:如果你用defvar 声明一个特定的名称,那么该名称将被动态绑定即使它们是函数的参数。这可能导致一些极其难以跟踪的错误,这些错误以极其令人困惑的方式表现出来(您基本上得到了错误的值,而且您不知道为什么会发生这种情况)。有经验的 Common Lispers 知道这一点(尤其是那些被它烧毁的人),并且将始终遵循在动态范围名称周围使用星号的约定(例如,*foo*)。 (顺便说一下,在 Common Lisp 行话中,这些动态作用域的变量被称为“特殊变量”——这对新手来说是另一个混​​淆的来源。)

      第三点在之前的一些cmets中也有讨论。事实上,Rainer 对你所拥有的不同选项做了一个很好的总结,但他没有解释它到底有多难。问题是适当的尾调用优化 (TCO) 是 Scheme 中的基本概念之一。它是一种语言特性,而不仅仅是一种优化,这一点非常重要。 Scheme 中的典型循环表示为尾调用函数(例如,(define (loop) (loop))),需要正确的 Scheme 实现来实现 TCO,这将保证这实际上是一个无限循环而不是运行一小会直到你炸毁堆栈空间。这就是 Rainer 第一个非解决方案的全部精髓,也是他将其标记为“BAD”的原因。 他的第三个选项——将函数循环(表示为递归函数)重写为 Common Lisp 循环(dotimesdolist 和臭名昭著的loop)可以用于一些简单的情况,但代价非常高:事实上,Scheme 是一种具有适当 TCO 的语言,这不仅是该语言的基础——它也是本书的主要主题之一,因此这样做,您将完全失去这一点。此外,在某些情况下,您只是不能将 Scheme 代码转换为 Common Lisp 循环结构——例如,当你按照自己的方式阅读本书时,你将实现一个元循环解释器,它是一种迷你方案语言的实现。需要单击一下才能意识到此元评估器实现的语言本身正在执行 TCO如果您实现此评估器的语言本身正在执行 TCO。 (请注意,我说的是“简单”解释器——在本书后面的部分中,您将此评估器实现为类似于寄存器机器的东西,您可以明确地让它做 TCO。)所有这一切的底线,是这个评估器 - 当在 Common Lisp 中实现时 - 将导致语言本身不执行 TCO。熟悉这一切的人应该不会感到惊讶:毕竟,评估器的“循环性”意味着您正在实现一种语义非常接近宿主语言的语言——所以在这种情况下,您“继承" Common Lisp 语义而不是 Scheme TCO 语义。但是,这意味着您的迷你评估器现在已经瘫痪:它没有 TCO,因此无法进行循环!要获得循环,您需要在解释器中实现新的结构,这通常会使用 Common Lisp 中的迭代结构。但是现在您离书中的内容更远了,并且您投入了大量的精力大约将 SICP 中的想法实施到不同的语言中。另请注意,所有这些都与我提出的前一点有关:如果您遵循本书,那么您实现的语言将在词法范围内,使其远离 Common Lisp 宿主语言。所以总的来说,你完全失去了书中所说的“元循环评估器”中的“循环”属性。 (同样,这可能不会打扰您,但会损害整体学习体验。)总而言之,很少语言在实现语言语义方面接近 Scheme在语言内部作为一个非平凡的(例如,不使用eval)评估器that很容易。 事实上,如果你确实使用 Common Lisp,那么在我看来,Rainer 的第二个建议——使用支持 TCO 的 Common Lisp 实现——是最好的选择。然而,在 Common Lisp 中,这基本上是一种编译器优化:因此,您可能需要 (a) 了解实现中需要转向以实现 TCO 的旋钮,(b) 您需要确保 Common Lisp 实现实际上是在做适当的 TCO,而不仅仅是优化 self 调用(这是更简单的情况,几乎没有那么重要),(c) 你会希望执行 TCO 的 Common Lisp 实现可以在不破坏调试选项的情况下这样做(同样,因为这被认为是 Common Lisp 中的优化,然后打开此旋钮,编译器也可能会认为“我不太在意可调试性”)。

      最后,我的最后一点并不难克服,但它在概念上是最重要的。在 Scheme 中,您有一个统一的规则:标识符有一个值,它是由词法确定的——就是这样。这是一种非常简单的语言。在 Common Lisp 中,除了有时使用动态作用域和有时使用词法作用域的历史包袱之外,您还有具有 两个 不同值的符号——每当变量出现在表达式的头部,并且有一个 不同的 值用于其他情况。例如,在(foo foo) 中,foo 的两个实例中的每一个都有不同的解释——第一个是foo 的函数值,第二个是它的变量值。同样,这并不难克服——您需要了解许多结构来处理所有这些问题。例如,不要写(lambda (x) (x x)),而是写(lambda (x) (funcall x x)),这会使被调用的函数出现在变量位置,因此在那里将使用相同的值;另一个例子是(map car something),你需要将它翻译成(map #'car something)(或者更准确地说,你需要使用mapcar,它是Common Lisp的car函数的等价物);你需要知道的另一件事是let 绑定了名称的值槽,而labels 绑定了函数槽(并且具有非常不同的语法,就像defundefvar。) 但所有这一切的概念结果是,Common Lispers 倾向于使用比 Schemers 少得多的高阶代码,这从每种语言中常见的习语一直到实现将用它做什么。 (例如,许多 Common Lisp 编译器永远不会优化这个调用:(funcall foo bar),而 Scheme 编译器会像任何函数调用表达式一样优化 (foo bar),因为没有其他方法可以调用函数。)

      最后,我要指出,以上大部分内容都是非常好的火焰战材料:将这些问题中的任何一个扔到公共 Lisp 或 Scheme 论坛(特别是 comp.lang.lispcomp.lang.scheme),你很可能看到一个长线程,人们解释为什么他们的选择比另一个更好,或者为什么某些“所谓的功能”实际上是由当时显然非常醉酒的语言设计者做出的一个愚蠢的决定,等等。但是问题是这些只是两种语言之间的差异,最终人们可以用任何一种语言完成工作。碰巧的是,如果工作是“做 SICP”,那么从 Scheme 的角度考虑它是如何解决这些问题的,Scheme 会容易得多。如果你想学习 Common Lisp,那么使用 Common Lisp 教科书会让你不那么沮丧。

      【讨论】:

      • 一口气读完了;你做了很好的解释,而且切中要害。我对 Scheme 不太熟悉,您所做的某些说明将有助于我在学习 Scheme 时从 CL 过渡。
      • 能否参考一下Scheme的TCO?你有类似r6rs.org/final/html/r6rs/r6rs-Z-H-14.html#node_sec_11.20 的想法吗?
      • 是的,所有方案都需要进行尾调用优化。这是一个非常基本的要求,以至于有些人不喜欢使用“优化”,因为它是一个实际的功能,而不仅仅是为了加快速度——所以一个较新的术语是尾调用消除。
      • +1:感谢您的详细回答,也因为它指出了我感兴趣的 Common Lisp 和 Scheme 之间的一些差异。
      • 如果 CL 实现实际上在尾递归函数上溢出,那将是一个拖累。我使用 SBCL 和 CLISP,但它们都没有表达式问题,否则 AFAICS 会炸毁他们的堆栈
      猜你喜欢
      • 2015-10-18
      • 2011-04-20
      • 1970-01-01
      • 1970-01-01
      • 2021-09-20
      • 2011-10-17
      • 2021-08-11
      • 1970-01-01
      • 2016-08-25
      相关资源
      最近更新 更多