我可以将Common Lisp用于SICP还是Scheme是唯一的选择? [英] Can I use Common Lisp for SICP or is Scheme the only option?

查看:80
本文介绍了我可以将Common Lisp用于SICP还是Scheme是唯一的选择?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

即使我可以使用Common Lisp,也应该吗? Scheme更好吗?

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

推荐答案

您在这里有几个答案,但没有一个是真正全面的(我不是在谈论具有足够的细节或足够长的时间).首先,最重要的是:如果您想拥有SICP的良好经验,则应该使用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.

如果您对Common Lisp的了解不多,那就以它为准. (很明显,您可以不顾其他任何建议而忽略此建议,有些人只能通过艰难的方式来学习.)

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

如果您已经了解Common Lisp,则可以将其付诸实践,但要付出很大的努力,并且会对您的整体学习经历造成很大的损害.有一些基本问题将Common Lisp和Scheme分开,这使得试图将前者与SICP一起使用是一个非常糟糕的主意.实际上,如果您具备使它起作用的知识水平,那么您无论如何都可能已经超过了SICP的水平.我并不是说这是不可能的-就像在C或Perl或任何其他方式中一样,可以在Common Lisp中实现整本书(例如,请参阅Bendersky的页面).除了Scheme之外,其他语言也会变得更加困难. (例如,即使ML的语法非常不同,它也可能比Common Lisp更易于使用.)

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和相关问题,以及不同的名称.

  1. NIL and related issues, and different names.

动态范围.

尾部呼叫优化.

用于函数和值的单独名称空间.

Separate namespace for functions and values.

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

I'll expand now on each of these points:

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

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

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

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

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

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").

最后,我的最后一点并不是很难克服,但从概念上讲这是最重要的一点.在Scheme中,您有一个统一的规则:标识符具有一个值,该值由词法确定-并且就是这样.这是一种非常简单的语言.在Common Lisp中,除了有时使用动态作用域和有时使用词法作用域的历史包g之外,您还拥有具有两个两个不同值的符号-每当变量出现在该符号上时,都会使用该函数值表达式的开头,还有一个 different 值,否则将使用该值.例如,在(foo foo)中,对foo的两个实例的解释各不相同-第一个是foo的函数值,第二个是其变量值.同样,这并不难克服-处理所有这些都需要了解许多构造.例如,代替编写(lambda (x) (x x)),您需要编写(lambda (x) (funcall x x)),这将使被调用的函数出现在可变位置,因此此处将使用相同的值.另一个示例是(map car something),您将需要将其翻译为(map #'car something)(或更准确地说,您将需要使用mapcar,它与car函数的Common Lisp等效);您还需要知道的另一件事是,let绑定了名称的值槽,而labels绑定了功能槽(并且具有非常不同的语法,就像defundefvar一样.) 但是所有这些的概念性结果是,Common Lispers使用的高阶代码往往比Schemers少得多,并且从每种语言中常见的习语一直到实现将如何使用它. (例如,许多Common Lisp编译器将永远不会优化此调用:(funcall foo bar),而Scheme编译器将像任何函数调用表达式一样优化(foo bar),因为没有 方式可以调用函数.)

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

最后,我要指出的是,以上大部分内容都是非常出色的资料:将这些问题中的任何一个放入公共Lisp或Scheme论坛(尤其是comp.lang.lispcomp.lang.scheme),您很可能看到一条长长的线索,人们在其中解释他们的选择为何比其他选择要好得多,或者为什么某些所谓的功能"实际上是由当时显然很醉的语言设计师所做出的愚蠢决定,等等.事实是,这只是两种语言之间的差异,最终人们可以用任何一种语言完成工作.碰巧的是,如果这项工作是在做SICP",那么从Scheme角度考虑如何解决这些问题,Scheme会容易得多.如果您想学习Common Lisp,那么使用Common Lisp教科书会让您沮丧得多.

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.

这篇关于我可以将Common Lisp用于SICP还是Scheme是唯一的选择?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆