我可以使用 Common Lisp 进行 SICP 还是 Scheme 是唯一的选择?

发布于 2024-07-29 13:07:03 字数 43 浏览 6 评论 0原文

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

Also, even if I can use Common Lisp, should I? Is Scheme better?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

予囚 2024-08-05 13:07:03

您在这里有几个答案,但没有一个是真正全面的(而且我不是在谈论有足够的细节或足够长)。 首先,底线是:如果您想获得良好的 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 既用作空列表,也用作假值。 就其本身而言,这并不是一个大问题,事实上 SICP 的第一版也有类似的假设——空列表和 false 是相同的值。 然而,Common Lisp 的 NIL 仍然不同:它也是一个符号。 因此,在Scheme中你有一个明确的分离:某个东西要么是一个列表,要么是值的基本类型之一——但在Common Lisp中,NIL不仅是假的并且是空列表:它是也是一个符号。 除此之外,您还会得到许多略有不同的行为 - 例如,在 Common Lisp 中,空列表的头部和尾部(carcdr)本身就是空列表,而在Scheme中,如果你尝试这样做,你会得到一个运行时错误。 最重要的是,你有不同的名称和命名约定,例如——Common Lisp 中的谓词按照约定以 P 结尾(例如,listp),而 Scheme 中的谓词 end用问号(例如,list?); Common Lisp 中的变异器没有特定的约定(有些具有 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 术语中,这些动态作用域的变量被称为“特殊变量”——这是新手混淆的另一个来源。)

第三点也在之前的一些评论中讨论过。 事实上,雷纳对你所拥有的不同选择做了很好的总结,但他没有解释它会让事情变得多么困难。 事实是,适当的尾部调用优化(TCO)是Scheme 中的基本概念之一。 它是一种语言特性,而不仅仅是一种优化,这一点非常重要。 Scheme 中的典型循环表示为尾部调用函数(例如,(define (loop) (loop))),并且需要正确的 Scheme 实现来实现 TCO这将保证这实际上是一个无限循环,而不是运行一小段时间直到耗尽堆栈空间。 这就是赖纳第一个非解决方案的本质,也是他将其标记为“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)评估器很容易。
事实上,如果您确实使用 Common Lisp,那么在我看来,Rainer 的第二个建议——使用支持 TCO 的 Common Lisp 实现——是最好的选择。 然而,在 Common Lisp 中,这从根本上来说是一种编译器优化:因此您可能需要 (a) 了解实现中为实现 TCO 所需的旋钮,(b) 您需要确保 Common Lisp Lisp 实现实际上是在进行适当的 TCO,而不仅仅是优化 self 调用(这是更简单的情况,但并不那么重要),(c) 您希望执行 TCO 的 Common Lisp 实现可以在不损坏调试选项的情况下执行此操作(同样,由于这被认为是 Common Lisp 中的优化,因此打开此旋钮,编译器也可能会认为“我不太关心为了可调试性”)。

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

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

You have several answers here, but none is really comprehensive (and I'm not talking about having enough details or being long enough). First of all, the bottom line: you should not use Common Lisp if you want to have a good experience with SICP.

If you don't know much Common Lisp, then just take it as that. (Obviously you can disregard this advice as anything else, some people only learn the hard way.)

If you already know Common Lisp, then you might pull it off, but at considerable effort, and at a considerable damage to your overall learning experience. There are some fundamental issues that separate Common Lisp and Scheme, which make trying to use the former with SICP a pretty bad idea. In fact, if you have the knowledge level to make it work, then you're likely above the level of SICP anyway. I'm not saying that it's not possible -- it is of course possible to implement the whole book in Common Lisp (for example, see Bendersky's pages) just as you can do so in C or Perl or whatever. It's just going to harder with languages that are further apart from Scheme. (For example, ML is likely to be easier to use than Common Lisp, even when its syntax is very different.)

Here are some of these major issues, in increasing order of importance. (I'm not saying that this list is exhaustive in any way, I'm sure that there are a whole bunch of additional issues that I'm omitting here.)

  1. NIL and related issues, and different names.

  2. Dynamic scope.

  3. Tail call optimization.

  4. Separate namespace for functions and values.

I'll expand now on each of these points:

The first point is the most technical. In Common Lisp, NIL is used both as the empty list and as the false value. In itself, this is not a big issue, and in fact the first edition of SICP had a similar assumption -- where the empty list and false were the same value. However, Common Lisp's NIL is still different: it is also a symbol. So, in Scheme you have a clear separation: something is either a list, or one of the primitive types of values -- but in Common Lisp, NIL is not only false and the empty list: it is also a symbol. In addition to this, you get a host of slightly different behavior -- for example, in Common Lisp the head and the tail (the car and cdr) of the empty list is itself the empty list, while in Scheme you'll get a runtime error if you try that. To top it off, you have different names and naming convention, for example -- predicates in Common Lisp end by convention with P (eg, listp) while predicates in Scheme end in a question mark (eg, list?); mutators in Common Lisp have no specific convention (some have an N prefix), while in Scheme they almost always have a suffix of !. Also, plain assignment in Common Lisp is usually setf and it can operate on combinations too (eg, (setf (car foo) 1)), while in Scheme it is set! and limited to setting bound variables only. (Note that Common Lisp has the limited version too, it's called setq. Almost nobody uses it though.)

The second point is a much deeper one, and possibly one that will lead to completely incomprehensible behavior of your code. The thing is that in Common Lisp, function arguments are lexically scoped, but variables that are declared with defvar are dynamically scoped. There is a whole range of solutions that rely on lexically scoped bindings -- and in Common Lisp they just won't work. Of course, the fact that Common Lisp has lexical scope means that you can get around this by being very careful about new bindings, and possibly using macros to get around the default dynamic scope -- but again, this requires a much more extensive knowledge than a typical newbie has. Things get even worse than that: if you declare a specific name with a defvar, then that name will be bound dynamically even if they're arguments to functions. This can lead to some extremely difficult to track bugs which manifest themselves in an extremely confusing way (you basically get the wrong value, and you'll have no clue why that happens). Experienced Common Lispers know about it (especially those that have been burnt by it), and will always follow the convention of using stars around dynamically scoped names (eg, *foo*). (And by the way, in Common Lisp jargon, these dynamically scoped variables are called just "special variables" -- which is another source of confusion for newbies.)

The third point was also discussed in some of the previous comments. In fact, Rainer had a pretty good summary of the different options that you have, but he didn't explain just how hard it can make things. The thing is that proper tail-call-optimization (TCO) is one of the fundamental concepts in Scheme. It is important enough that it is a language feature rather than merely an optimization. A typical loop in Scheme is expressed as a tail-calling function (for example, (define (loop) (loop))) and proper Scheme implementations are required to implement TCO which will guarantee that this is, in fact, an infinite loop rather than running for a short while until you blow up the stack space. This is all the essence of Rainer's first non solution, and the reason he labeled it as "BAD".
His third option -- rewriting functional loops (expressed as recursive functions) as Common Lisp loops (dotimes, dolist, and the infamous loop) can work for a few simple cases, but at a very high cost: the fact that Scheme is a language that does proper TCO is not only fundamental to the language -- it is also one of the major themes in the book, so by doing so, you will have lost that point completely. In addition, there are some cases that you just cannot translate Scheme code into a Common Lisp loop construct -- for example, as you work your way through the book, you'll get to implement a meta-circular-interpreter which is an implementation of a mini-Scheme language. It takes a certain click to realize that this meta evaluator implements a language that is itself doing TCO if the language that you implement this evaluator in is itself doing TCO. (Note that I'm talking about the "simple" interpreters -- later in the book you implement this evaluator as something close to a register machine, where you kind of explicitly make it do TCO.) The bottom line to all of this, is that this evaluator -- when implemented in Common Lisp -- will result in a language that is itself not doing TCO. People who are familiar with all of this should not be surprised: after all, the "circularity" of the evaluator means that you're implementing a language with semantics that are very close to the host language -- so in this case you "inherit" the Common Lisp semantics rather than the Scheme TCO semantics. However, this means that your mini-evaluator is now crippled: it has no TCO, so it has no way of doing loops! To get loops in, you will need to implement new constructs in your interpreter, which will usually use the iteration constructs in Common Lisp. But now you're going further away from what's in the book, and you're investing considerable effort in approximately implementing the ideas in SICP to the different language. Note also that all of this is related to the previous point I raised: if you follow the book, then the language that you implement will be lexically scoped, taking it further away from the Common Lisp host language. So overall, you completely lose the "circular" property in what the book calls "meta circular evaluator". (Again, this is something that might not bother you, but it will damage the overall learning experience.) All in all, very few languages get close to Scheme in being able to implement the semantics of the language inside the language as a non-trivial (eg, not using eval) evaluator that easily.
In fact, if you do go with a Common Lisp, then in my opinion, Rainer's second suggestion -- use a Common Lisp implementation that supports TCO -- is the best way to go. However, in Common Lisp this is fundamentally a compiler optimization: so you will likely need to (a) know about the knobs in the implementation that you need to turn to make TCO happen, (b) you will need to make sure that the Common Lisp implementation is actually doing proper TCO, and not just optimization of self calls (which is the much simpler case that is not nearly as important), (c) you would hope that the Common Lisp implementation that does TCO can do so without damaging debugging options (again, since this is considered an optimization in Common Lisp, then turning this knob on, might also be taken by the compiler as saying "I don't care much for debuggability").

Finally, my last point is not too hard to overcome, but it is conceptually the most important one. In Scheme, you have a uniform rule: identifiers have a value, which is determined lexically -- and that's it. It's a very simple language. In Common Lisp, in addition to the historical baggage of sometimes using dynamic scope and sometimes using lexical scope, you have symbols that have two different value -- there's the function value that is used whenever a variable appears at the head of an expression, and there is a different value that is used otherwise. For example, in (foo foo), each of the two instances of foo are interpreted differently -- the first is the function value of foo and the second is its variable value. Again, this is not hard to overcome -- there are a number of constructs that you need to know about to deal with all of this. For example, instead of writing (lambda (x) (x x)) you need to write (lambda (x) (funcall x x)), which makes the function that is being called appear in a variable position, therefore the same value will be used there; another example is (map car something) which you will need to translate to (map #'car something) (or more accurately, you will need to use mapcar which is Common Lisp's equivalent of the car function); yet another thing that you'll need to know is that let binds the value slot of the name, and labels binds the function slot (and has a very different syntax, just like defun and defvar.)
But the conceptual result of all of this is that Common Lispers tend to use higher-order code much less than Schemers, and that goes all the way from the idioms that are common in each language, to what implementations will do with it. (For example, many Common Lisp compilers will never optimize this call: (funcall foo bar), while Scheme compilers will optimize (foo bar) like any function call expression, because there is no other way to call functions.)

Finally, I'll note that much of the above is very good flamewar material: throw any of these issues into a public Lisp or Scheme forum (in particular comp.lang.lisp and comp.lang.scheme), and you'll most likely see a long thread where people explain why their choice is far better than the other, or why some "so called feature" is actually an idiotic decision that was made by language designers that were clearly very drunk at the time, etc etc. But the thing is that these are just differences between the two languages, and eventually people can get their job done in either one. It just happens that if the job is "doing SICP" then Scheme will be much easier considering how it hits each of these issues from the Scheme perspective. If you want to learn Common Lisp, then going with a Common Lisp textbook will leave you much less frustrated.

病毒体 2024-08-05 13:07:03

将 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),并非所有编译器都支持它(JVM 上的某些 Lisp Machine 和 ABCL 不支持) ,它使一些调试/跟踪/单步变得更加困难,...)。

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

  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 编写的旧方案实现(称为伪方案),它应该运行 SICP 中的大部分代码。

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

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

示例

让我们看一下 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 有一个用于微分的方案示例:

(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 CL:COND 中的 numberp
  • 使用 T 而不是 else
  • CL:ERROR 使用CL 格式字符串

让我们为一些函数定义方案名称。 Common 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第3.5章中的书籍代码。 我们使用上面对 CL 的添加。

SICP提到了delay、the-empty-stream和cons-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

Using SICP with Common Lisp is possible and fun

You can use Common Lisp for learning with SICP without much problems. The Scheme subset that is used in the book is not very sophisticated. SICP does not use macros and it uses no continuations. There are DELAY and FORCE, which can be written in Common Lisp in a few lines.

Also for a beginner using (function foo) and (funcall foo 1 2 3) is actually better (IMHO !), because the code gets clearer when learning the functional programming parts. You can see where variables and lambda functions are being called/passed.

Tail call optimization in Common Lisp

There is only one big area where using Common Lisp has a drawback: tail call optimization (TCO). Common Lisp does not support TCO in its standard (because of unclear interaction with the rest of the language, not all computer architectures support it directly (think JVM), not all compilers support it (some Lisp Machine and ABCL on the JVM do not), it makes some debugging/tracing/stepping harder, ...).

There are three ways to live with that:

  1. Hope that the stack does not blow out. BAD.
  2. Use a Common Lisp implementation that supports TCO. There are many compilers which do. See below.
  3. Rewrite the functional loops (and similar constructs) into loops (and similar constructs) using DOTIMES, DO, LOOP, ...

Personally I would recommend 2 (or 3).

Common Lisp has excellent and easy to use compilers with TCO support (SBCL, LispWorks, Allegro CL, Clozure CL, ...) and as a development environment use either the built-in ones or GNU Emacs/SLIME.

For use with SICP I would recommend SBCL, since it compiles always by default, has TCO support by default and the compiler catches a lot of coding problems (undeclared variables, wrong argument lists, a bunch of type errors, ...). This helps a lot during learning. Generally make sure the code is compiled, since Common Lisp interpreters will usually not support TCO.

Sometimes it might also helpful to write one or two macros and provide some Scheme function names to make code look a bit more like Scheme. For example you could have a DEFINE macro in Common Lisp.

For the more advanced users, there is an old Scheme implementation written in Common Lisp (called Pseudo Scheme), that should run most of the code in SICP.

My recommendation: if you want to go the extra mile and use Common Lisp, do it.

To make it easier to understand the necessary changes, I've added a few examples - remember, it needs a Common Lisp compiler with support for tail call optimization like SBCL:

Example

Let's look at this simple code from 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)))

We can use it directly in Common Lisp with a DEFINE macro:

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

Now you should use SBCL, CCL, Allegro CL or LispWorks. Their compilers support TCO by default.

Let's use 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....

Another Example: symbolic differentiation

SICP has a Scheme example for differentiation:

(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))))

Making this code run in Common Lisp is easy:

  • some functions have different names, number? is numberp in CL
  • CL:COND uses T instead of else
  • CL:ERROR uses CL format strings

Let's define Scheme names for some functions. Common Lisp code:

(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)))

Our define macro from above:

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

The Common Lisp code:

(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))))

Let's try it in LispWorks:

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

Streams example from SICP in Common Lisp

See the book code in chapter 3.5 in SICP. We use the additions to CL from above.

SICP mentions delay, the-empty-stream and cons-stream, but does not implement it. We provide here an implementation in 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"))

Now comes portable code from the book:

(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))))

Now Common Lisp differs in stream-for-each:

  • we need to use cl:progn instead of begin
  • function parameters need to be called with cl:funcall

Here is a version:

(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)))))

We also need to pass functions using cl:function:

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

But then the example works:

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
永不分离 2024-08-05 13:07:03

您已经了解一些 Common Lisp 了吗? 我想这就是你所说的“Lisp”的意思。 在这种情况下,您可能想使用它而不是Scheme。 如果您两者都不知道,并且您通过 SICP 纯粹是为了获得学习经验,那么您可能会更适合使用Scheme。 它为新学习者提供了更好的支持,并且您无需从Scheme 翻译为Common Lisp。

存在差异; 具体来说,SICP 的高度函数式风格在 Common Lisp 中更加冗长,因为在传递函数时必须引用函数并使用 funcall 来调用绑定到变量的函数。

但是,如果您想使用 Common Lisp,可以尝试使用 Eli Bendersky 对 SICP 代码的 Common Lisp 翻译位于 SICP 标签下。

Do you already know some Common Lisp? I assume that is what you mean by 'Lisp'. In that case you might want to use it instead of Scheme. If you don't know either, and you are working through SICP solely for the learning experience, then probably you are better off with Scheme. It has much better support for new learners, and you won't have to translate from Scheme to Common Lisp.

There are differences; specifically, SICP's highly functional style is wordier in Common Lisp because you have to quote functions when passing them around and use funcall to call a function bound to a variable.

However, if you want to use Common Lisp, you can try using Eli Bendersky's Common Lisp translations of the SICP code under the tag SICP.

尛丟丟 2024-08-05 13:07:03

它们相似但不相同。

我相信如果你选择Scheme的话会更容易。

They are similar but not the same.

I believe If you go with Scheme it would be easier.

月棠 2024-08-05 13:07:03

编辑:内森·桑德斯的评论是正确的。 距离我上次读这本书显然已经有一段时间了,但我刚刚检查了一下,它并没有直接使用 call/cc 。 我对内森的回答投了赞成票。


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

Edit: Nathan Sanders' comment is correct. It's clearly been a while since I last read the book, but I just checked and it does not use call/cc directly. I've upvoted Nathan's answer.


Whatever you use needs to implement continuations, which SICP uses a lot. Not even all Scheme interpreters implement them, and I'm not aware of any Common Lisp that does.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文