Scheme 语言介绍下

来自 ChinaUnix Wiki

目录

Lambda 表达式和环境

Scheme 的执行环境被表示为所谓的“lambda 表达式”。这个术语起源于 Church 的 lambda 演算(calculus),它充当 MacCarthy 在符号计算上的某些想法的模型。Lambda 表达式开始于一个关键字“lambda”,随后式一个(可能为空)参数的列表和一个表达式序列。当 lambda 表达式应用在正确的上下文中的时候,这些表达式按顺序执行并返回最后的值。

       (lambda () "hi there")
       ;#
       ( (lambda () "hi there") )
       ;"hi there"

在第一个例子中的 lambda 表达式定义过程文字。在第二个 lambda 表达式周围包装了额外的一对括号强制它执行。Lambda 表达式可以接受固定或可变数目的参数。使用不同的语法惯例来指出需要那种参数传递机制。

       ((lambda (aFriend) ; 函数有一个形式参数: aFriend
           (DisplayLine "hi there " aFriend))
        'HappyMole) ; 调用时带有一个实际参数: HappyMole
       ;hi there HappyMole #t

这里把由 lambda 表达式表示的过程应用到作为它的参数的 "HappyMole"上。这生成一个计算,在其中 DisplayLine 把要求的字符串放置到屏幕上。#t 是最后的显示返回的值,因此它还是整个 lambda 表达式的返回值。如果你在你自己的平台上写过了这个例子,你最有可能忘记引用 HappyMole,导致 Scheme 抱怨另一个 "unbound variable"。得到正确的引用可能需要很多实践,但是不久之后它就会成为“第二天性”。

Scheme 提供两种可供选择的方式来要求可变数目的实际参数。

       ; lambda 表达式,调用时带有 3 个实际参数
       ((lambda someFriends  ; 参数周围没有括号 !
           (DisplayLine "hi there " someFriends))
        'mole 'bear 'tiger)
       ;hi there (mole bear tiger) #t
       ; 调用时带有 0 个实际参数
       ((lambda someFriends
           (DisplayLine "hi there" someFriends)) )
       ;hi there () #t  

下面的所有参数都绑定到一个表(它可以为空)中并传递给 lambda 表达式。请注意我们必须提供一个不带任何括号的单一的参数来导致这种行为。如果我们想要让我们所有的参数都是可选择的,可以使用

       ; 一个强制的和一些可选的参数
       ((lambda (aFriend . someMore)
           (DisplayLine "hi there " someMore))
        'mole 'bear 'tiger) ; 三个参数
       ;hi there (bear tiger) #t  ; mole 现在被绑定到 "aFriend"
       ((lambda (aFriend . someMore)
           (DisplayLine "hi there" someMore)))
           ; 没有参数
       ;ERROR: Too few arguments to procedure
       ;0 arguments supplied - 1 argument expected

这里的 aFriend 将被绑定为 mole,而所有余下的实际参数将再次被组合到一个表中并绑定到 someMore。 当然,我们现在必须提供最少一个实际参数。

Scheme 提供一个谓词(procedure?)来测试一个对象实际上是否为过程。apply 在某些情况下也是有用的,它强制一个过程在当前上下文中的应用。注意 apply 总是期望一个表作为它的单一的参数。

       (procedure? (lambda () (display "hello")))
       ;#t
       (apply (lambda (aFriend)
                (DisplayLine "hello" aFriend))
              '(mole) )
       ;hello mole #t

Lambda 表达式可以包含它们自己的对数据和过程的“局部”定义。因此它们是主要的的环境建造块, Scheme 依据“词法作用域”的概念而有层次的安排它们。使用这种环境来定义在一个计算期间的所有点上(“作用域内”)都是当前可见的对象,早期的 Lisp 系统只支持“动态作用域”,这是导致无数非常难于跟踪的程序错误的一个“特征”。 执行的嵌套上下文

除了嵌套过程 Scheme 还有一个额外的构造用来定义(临时)环境。叫做“let 表达式”(let、let*、letrec)。生成一种“块结构”(类似于 Algol 60)。let 是一个特殊的形式,它接受绑定和一个“块体”作为参数。在这些绑定定义的上下文中,在块体中包含的表达式接着顺序的执行并返回最后一个的值。let* 假定所有绑定将被顺序的建立(从第一个到最后一个),而 let 不做这种假定。Letrec 对于相互递归的过程是需要的。

       (let ((seven 7) (thirteen 13))
          (DisplayLine (* seven thirteen)) (+ seven thirteen))
       ; 91 ; 乘法的结果
       ; 20 ; 加法的结果
       (let ((aNumber 7)
             (twiceThat (* 2 aNumber)))
         (display twiceThat) )
       ;ERROR: Undefined global variable aNumber
       (let* ((aNumber 7)
              (twiceThat (* 2 aNumber)))
         (display twiceThat) )
       ;14 #t

这里我们需要使用 let* 来使相互依赖的绑定工作。MacScheme 的 let 显然的尝试从右到左的绑定符号,因此在被定义之前就遇到了 aNumber。

过程 - 定义, 测试, 调试

绑定 lambda 表达式到标识符导出了“命名”过程的概念。典型的 Scheme 程序将包含大量用户定义的过程,通常组织到必须在程序执行之前装载的一些文件中。使用 load 过程完成这个任务。

       (load "HardDisk:cookieMonster.scm")

将尝试装载并求值在目录 HardDisk 中的文件 cookieMonster.scm 中的所有表达式。文件操纵的详情通常特定于特定 Scheme 实现。但是,习惯于对 Scheme 文件使用后缀“scm”。

我们可以“别名”任何数据类型,包括过程。它可以用来裁剪系统函数的名字来适合我们的偏好;带有额外的一层间接的代价。我们通常保护重要的系统定义的标识符不被意外的重定义。但是,对于经常需要的操作符(比如 car 和 cdr),导致的在执行速度上的损失通常是不可接受的高。

       (define head car)
       ;head
       (define tail  cdr)
       ;tail
       (head (tail '(vampire banshee troll)))
       ;banshee
       (set! + *) ; sneaky: redefine '+' as a multiplication symbol ?
       ;ERROR: Integrable procedures may not be redefined

请注意 Scheme 的绑定模型是完全一般性的。在对“被动”对象(数据)能做的事情和对“主动”对象(过程)能做的事情之间没有区别。例如,我们可以轻易的写返回另一个过程作为值的过程。

       (define GenericMagicLamp
          ; 这是一个我们希望返回的"lambda"表达式
          (lambda (noOfWishes)
             (define count 0) ;局部变量
                (lambda ()
                   (if (< count noOfWishes)
                       (begin
                         (set! count (+ 1 count))
                         'Granted)
                       '(you've had all you're going to get !))
                 ) ))
        ; GenericMagicLamp


GenericMagicLamp 实现了阿拉丁神灯的概念,在其中关押了一个妖魔并通过摩擦震动来释放出来。出于感激的天性,她将接着提供惯例的三个愿望,通常带有针对 meta-circularity 的某种保护(就是说,拒绝能带给你额外愿望的愿望)。我们的过程包含对一个局部变量 count 的声明,它将被初始化为零并在每次满足愿望之后增加一。在这一点上,注意到 Scheme 变量都有“无限的范围”是很重要的,这意味着,(例如)与 Pascal 中的其他变量不同,在它们在其中定义的过程的连续调用之间,它们不会失去它们的值。因此 Count 将开始于为零的一个值,在第一个愿望之后保持为一的一个值,第二次之后是二,在第三次调用之后是三。尽管 GenericMagicLamp 自身不会满足任何愿望,但是它可以用做一个发生器,用来构造任意数目的神灯,带有在创建时刻定义的愿望数目。这是嵌套在这个过程之中的第二个 lambda 表达式的作用。记住这个 Scheme 函数总是返回它们遇到的最后一个表达式的值。在这里是第二个 lambda 表达式,因此 GenericMagicLamp 是返回另一过程作为它的调用结果的一个过程。

       (define AlladinsLamp (GenericMagicLamp 3))
       ;AlladinsLamp

可以用来制造这么一个灯(一个过程)并让 AlladinsLamp 充当它的标识符。记住这个过程还继承了在创建发生的时候、它在其中建立的环境中的所有绑定变量是很重要的。Count 因此对 AlladinsLamp 是可以访问的,并且它仍被绑定为零。如果我们现在开始摩擦,我们就会得到我们期望的行为了(这些愿望的“形态”将保持私有)。

       (AlladinsLamp)
       ;granted
       (AlladinsLamp)
       ;granted
       (AlladinsLamp)
       ;granted
       (AlladinsLamp)
       ;"you've had all you're going to get !"
       (set! count 0)
       ;ERROR: Undefined global variable count
       ;hard luck  - "count" is  kept private to this lamp object, and can't be accessed !     

递归

递归具体化了自引用的概念,并且它是 Scheme 指定一段代码的重复执行的最有效的工具。阐明它的一般想法的一个好例子是众所周知的一个漫画,一个人拿着他自己的照片,在照片种他要年轻几岁。照片中的人依次拿着同样的照片(照片中的人拿着同样的照片 ...) 一直到“降到最底部”、照片中的人是个婴儿。

可以使用递归来以非常幽雅的方式描述结构和处理过程二者[Roberts (1986), Burge (1975)]。唯一的要求是我们可以对其使用递归的、事物的某种程度上的规律性。这种规律性必须包含在元素和它们的内部连接的本质中,它引发直接或间接的线性、层次、或网络结构。结构的递归描述是常见的。上面提及的照片就是一个例子。在科学和自然中很多结构是高度递归性的,通过它们的成长过程来探索某种规律性,在计算机科学同样业富于这种描述。考虑把二叉树规定作为“一个叶子或一对二叉树”;或把表规定为“一个空表或跟随着一个表的一个元素”。表的递归本质很容易的映射到通常处理它们的递归方式上。在图表 2.13 中展示了一种适用的情况。这里递归的调用 cdr 函数来“走”到我们感兴趣的元素那里。处理过程的递归描述在日常生活中也是很流行的。儿童的故事通常是高度递归的,而且这种描述好象非常容易理解和非常有趣(在那个年纪)。我们将查看下面的简单的例子。这种故事也非常容易“产生”(讲述)而很快变得无聊。

用数学归纳法证明的处理过程与递归描述密切相关。我们可以很非正式的把递归定义为把一个问题简化成在结构上是一致的、并某种程度上更加容易解决的一个或多个子问题的处理过程。这种想法可以接着应用到每个子问题,直到整个处理过程直到子问题的解决变得明显的的某个层次。我们可以接着“递归回溯”子问题链来把所有东西再次合到一起,所以对最初问题的解决将被构造出来。Hofstadter (1983) 给出了这种处理过程的愚蠢但有启迪意义的一个例子: “你如何做有 13 个石块的一个堆? - 放置一个石块在有 12 个石块的一个堆上。你如何做有 12 个石块的一个堆上? ..."。我们将把任务的完成留做读者的练习。当然我们必须总是小心的提供某种方式,能够预期这种描述或处理过程终止。想象一下执行了下列程序之后会发生什么 [Dybvig (1987), 37] (它的名字告诉我们不要尝试它)。

       (define GoodBye (lambda () (display ".") (GoodBye)))
       (GoodBye)

可以用迭代表示的所有东西都可以捕获到等价的递归公式中。与其他语言不同的是在 Scheme 中使用递归经常没有存储空间的处罚,因为解释器将自动的尝试把递归描述转换到一个迭代循环结构中。这总是由所谓的“尾部递归”来保证。如果在递归调用点上的一个过程的值是这个递归调用的值,则这个调用是尾部递归的。但是,如果在把这个结果“向上”传递回递归链之前对它进行了某些操作,则尾部递归不成立 [Dybvig (1987), 71]。在需要对表元素的操作的重复性应用的时候,还有在其他通常使用重复结构(比如 do, for, while)的上下文中,尾部递归发生的非常频繁。因次 Scheme 的迭代过程只是充当语法糖衣。

在 Scheme 中递归最通常用于表处理。考虑一个程序,写众所周知的“Hairy MacLary”类型的儿童故事的[Dodd (undated)]一个变种。在这些故事中 Hairy McLary(一个 terrier 狗)出去散步并沿途“积累”朋友,所以在每个门口唱诵的名字表将逐渐变长。

       (define StoryTeller
         (lambda (mainCharacter someFriends)
           (define chorus
              (lambda (someFriends caravan)
                 ; 检查终止
                 (if (null? someFriends)
                     #f
                     (begin
                       ; 问候一个朋友
                       (DisplayLine (car someFriends))
                       ; 并“罗列”所有已经加入的人
                       (display "with ")
                       (map (lambda (aFriend)
                               (DisplayLine aFriend)
                               (display "and "))
                            caravan)
                       (DisplayLine "...")
                       ; 在另一个朋友上递归(并让这个人
                       ; 加入 "caravan")
                       (chorus (cdr someFriends)
                               (append
                                  caravan
                                  ; needs a list here !
                                 (list (car someFriends))) ))) ))
       ; "StoryTeller" 函数的函数体
        (DisplayLine "out of the gate and off for a walk went")
        (DisplayLine mainCharacter " ...")
        (newline)
        (chorus someFriends (list mainCharacter))
        ; we will skip some more friends here, and also their
        ; encounter with Scarface Claw (the toughest Tom in town)
        (newline)
        (display "straight back home to bed") ))
        ;StoryTeller

在做调用带有如下数据的时候,这个尾部递归过程生成下面的熟悉的故事:

       (StoryTeller
        "Hairy MacLary from Donaldson's Dairy"
        '("Hercules Morse, as big as a horse"
          "Bottomley Potts, covered in spots"
          "Muffin McLay, like a bundle of hay"))
          ; out of the gate and off for a walk went
          ; Hairy MacLary from Donaldson's Dairy ...
          ; Hercules Morse, as big as a horse
          ; with Hairy MacLary from Donaldson's Dairy
          ; and ...
          ; Bottomley Potts, covered in spots
          ; with Hercules Morse, as big as a horse
          ; and Hairy MacLary from Donaldson's Dairy
          ; and ...
          ; Muffin McLay, like a bundle of hay
          ; with Bottomley Potts, covered in spots
          ; and Hercules Morse, as big as a horse
          ; and Hairy MacLary from Donaldson's Dairy
          ; and ...
          ; straight back home to bed#t

上面的程序是尾部递归的,因为我们在后续对 chorus 的调用返回之后不尝试更改它生成的任何数据。它还对展示非尾部递归的过程是有教益的。

这次我们的朋友们攒钱买一个新沙发,他们定时地希望总计他们的钱看是否已经买得起了。下面的过程做这个把戏,假定我们列出朋友和有关的财产。

       (define HowMuchMoneyDoWeHave
         (lambda (someFriends)
          (define count
             (lambda (aFriend) (cadr aFriend)))
          (trace count)
          ; "HowMuchMoneyDoWeHave"过程的过程体
          (if (null? someFriends) ; 终止条件
              0
              ; 把它们加到一起
              (+ (count (car someFriends)) ; 这个人
                 (HowMuchMoneyDoWeHave ((cdr someFriends))) ; 其他人的
           ) ))
        ;HowMuchMoneyDoWeHave
        (trace HowMuchMoneyDoWeHave)
         ; #t

下面的跟踪展示在程序执行期间递归是如何展开的。跟踪功能不是标准的 Scheme 特征,但多数方言都提供了这种设施。

     (HowMuchMoneyDoWeHave
        '((HappyMole 3) (LittleBear 1) (LittleTiger 0)))

将打印:

       ; 递归“进入”表
           Computing (#
                      ((happymole 3) (littlebear 1) (littletiger 0)))
                Computing (#
                           ((littlebear 1) (littletiger 0)))
                   Computing (#
                              ((littletiger 0)))
                      Computing (#
                                 ())
                 ; 终止条件满足了,
                 ; “回绕”这个递归 (做我们要的总计)
                 (# ()) --> 0
              Computing (# (littletiger 0))
                (# (littletiger 0)) --> 0
                        (#
                                ((littletiger 0))) --> 0
                Computing (# (littlebear 1))
                  (# (littlebear 1)) --> 1
                (#
                     ((littlebear 1) (littletiger 0))) --> 1
                  Computing (# (happymole 3))
                 (# (happymole 3)) --> 3
                               (#
                    ((happymole 3) (littlebear 1) (littletiger 0)))
                     --> 4
           ; 4

运气不好,沙发价值 10 个金币,他们仍不能买一个。如同预想的那样你不能太依赖 tiger。他对金钱没有什么想法。

额外特征

本节总结对 Scheme 的简要介绍。尽管多数 Scheme 的主要特征已经被覆盖了,没有空间做更多的风格讨论了。我们故意把我们的处置限制在据信是 Scheme 的本质的和可移植的那些方面。Rees 和 Clinger (1986)给出这门语言的完整定义。有些额外的标准特征,特别是在数值和输入/输出操作领域中。概念,比如引擎[Dybvig (1987),Hayes 和 Friedman (1984)],宏和语法扩展[Dybvig (1987)]没有提及;主要是因为仍然缺乏这些概念的标准实现,尽管很多方言已经提供了一些这种设施。请注意本书不意图充当参考手册的替代品。因此我们强烈建议使用你工作用的方言的参考手册,接着继续你的探索。

编程是需要实践的一种技巧,特别是对于“良好”风格的开发。读其他人写的程序是特别重要的,然而写你自己的程序和从你的错误中学习是生死攸关的。

Scheme 好象特别适合于这种业务,因为它带给用户的不只是编码解决某些问题,还有寻找特别优雅的公式表达。Friedman 评论一个良好的计算机语言为:“应当提供一个环境,程序可以在其中为手头的问题创造性生成计算模型或范例。程序员构造范例的能力不应当受到限制。我把易于构造范例称为语言的“易范例性”(paradigmicity)。有低易范例性的语言是讨厌的。...我们如何建立有高易范例性的语言呢? 我们介入一些基本概念,组合这些概念的一些方式,并且这么做的过程中我们利用了解决问题的多年经验。在现存的语言中没有那个比 Scheme 更易范例性了。它的基本概念是过程、续体、引擎、条件和赋值语句。所有东西都会合在复合和递归下,它预备了语法和语义扩展二者。... 使用这些基本概念的实验和实践的越多,新范例出现的越快。” [Friedman in: Dybvig (1987), preface]。

对风格的一些建议

Scheme 是一个高度交互式的语言,它带有鼓励试探性风格的问题解决的特征。这种属性使学习过程变得容易。新的想法可以立即实验,它担当使困难的概念非神秘化的任务要比任何“静态”的描述形式要好。表结构用来表示数据和程序二者。因为它们还强调了有层次的嵌套和递归,这鼓励了“块状”的应用程序和创造分层设计。

有一些基本规则用来写有良好结构的 Scheme 程序,多数这些规则类似于在典型的 Lisp 教科书中给出的指导方针。Abelson 等人(1985) 的书和在本书中讨论的工具箱程序可以充当有代表性的例子。

过程定义应当简要,并且它们应当面向一个单一的、定义良好的目的。任务应当尽可能的分解到一些子任务中,如果它们延伸超过了一页,把它们委派给其他过程。过程还应当围绕概念来组织,概念依次反映在正确的数据结构中。这个要求演化自分层设计的方法论,强烈建议使用它。创建、查询、选择、显示和更改过程的概念给出对通常需要那种过程的指导。

程序应当是可读的和易于理解的。这个原理对标识符命名和程序构造方式有明显暗示。标识符应当是描述性的,并且它们应当提供与所表示的概念有关的助记符。还希望你遵从分类标识符的特定格式惯例。例如,我们使用大写字母来开始工具箱过程的名字,而小写字母用作系统定义的过程的名字。Scheme 使用特殊的后缀: "?" 用于谓词(就是返回 #t 或 #f 的函数)和 "!" 用于有“副作用”的所有过程(就是说,改变对非局部变量的绑定)。因为 Scheme 是一个动态类型的语言,参数的命名应当反映它们的值。使用 aNumber 或 anAList 替代 n 或 l。尽管你可能开始时憎恨键入长名字、并可能感觉你的程序过于冗长了,额外的努力以后会得到报答的。

在你的程序中使用清晰的结构将要求某些决断力,而你将最终发展出一种你自己的风格。完成这个目标的唯一方法是阅读和评判其他人的代码,并采纳你喜欢的到你写的程序中。因为这种决定通常涉及感性,除了维持一致性之外,不能给出一般的规则。不要惧怕实验。如果你觉得增进了的理解能使你做出更好的工作,抛弃老的程序并再次开始。

递归和表结构在编程符号应用中同迭代和数组在数值应用中一样常见。尽管你需要做有意识的努力来克服所有 Pascal 或 Fortran 条件反射,你应该使用它们发挥它们的全部优势。递归是定义和处理重复和有规律结构的“自然的”和优雅的方式,并且这种结构很易于表示为表。

set! 过程可能产生“非局部”的副作用,所以它有害于一个程序的可理解性。这导致一些人完全反对它的使用并鼓吹没有任何副作用的“纯”函数式编程。在处理过程要按照复杂的状态变化来描述的情况下,这种风格经常是非常不实际的,所以作者不赞同这种推理。改变值绑定的能力是非常有用的工具,它的作用不能总是很方便的用任何其他方式完成。你应当尽可能的细心的保持这种绑定的作用域为局部,并适当的加以文档。在很多情况下,let 表达式、嵌套过程调用和递归能导致更清晰的结构。

深度嵌套的 cars 和 cdrs 经常是难于理解的,因此应当避免。处理这种深度递归结构的更加可取的方法是使用多层解释,这样 car 和 cdr 的长链就变得不需要了。例如,(GetXCoordinate (GetFirstPoint (GetBottomEdge (aTriangle)))) 比 (caar (caddr aTriangle)))更可取。使用适当的选择子(selector)函数也会使你的程序更加灵活并更易于维护。对象封装的实质性利益也应当探究,因为使用这种象征(metaphor)有助于使你的程序更加健壮和安全。

象很多其他强力特征一样,续体可能是危险的工具。尽管它们在某种程度上比“go to”语句相关的想法更加“安全”,它们的正确使用在相当大程度上要求程序员方面的自我约束。对“why”、“what”、“where”和“how”加以适当的文档也是基本的,更甚于更简单的结构。因此续体应当保守的使用,用来实现其他方式不能实现的目标。即使你不应当以它们的“原始”形式使用它们,但是应当把它们看作组合适当的高层结构的建造块(比如,非局部 escape, 协同例程 ...)。

Scheme 的交互式本质允许你在过程写完之后理解测试它们。你应当完全利用这种可能性并运行一组选择的测试个例,有效的和无效的二者都要有。

在编程风格上的多数一般指导方针也适用于 Scheme 程序。Kernighan & Plauger (1974) 和 Ledgard (1974) 给出了一个良好的总结。

程序应当适当的缩进和注释,尽管使用适当的标识符和有可能迅速的测试过程减少了注释的数量但它们仍是需要的。过程的定义通常应当开始于一个对它功能的简要解释,如果从它的名字看来不是很明显的。在复杂的应用中还应当有对类型、结构和这个标识符用途的描述。除了这些考虑之外,应当使用注释来突出一段代码重要的部分。它们应当经常是解释性的(就是说,在做“什么”)而不是纯描述性(就是说,是“如何”完成的)。缩进应当是一致的并应当强调程序的结构。因为嵌套的过程的清单有不幸的倾向,它会溢出页面的右侧,在任何 scheme 缩紧之下,都经常需要行使某些调整,在一致性和美观性之间作出权衡。

所有信息都应当尽可能的模块化和局部化。全局变量应当非常保守的使用! Scheme 支持局部变量和局部过程,你应当使用这些设施来把信息装载到正确的模块中。例如,任何只在另一个过程内部调用的过程都应当嵌套到那个过程中。作为它的逻辑上的结论,这导致了面向对象结构,尽管有些事例中过程是简单的,在其中使用与面向对象有关的额外的“脚手架”是不正当的。

因为 Scheme 用作讨论很多重要的编程象征的一个工具,本书后面的章节将提供给出近一步提示和察看这些建议应用的充分机会。

总结和看法

Lisp 是围绕符号操纵的想法建造的。原子和表是它的基本结构建造块。每个对象要么是一个表要么是一个原子。表可以递归的定义,所以可以有表的表的表 ...,形成任意复杂的树结构,表和原子可以被求值为要么数据要么是所谓的 lambda 表达式。事实上,Lisp 解释器将求值它遇到的所有东西,除非被显式的引用了。

Lisp 是一个应用式和基于表达式的语言,这意味着解释的主体单元是圆括号表达式。“纯” Lisp 禁止“副作用”。这个限制经常被证明是过分限制、并且实际语言在很大程度上屏弃了它。所以 Lisp 通常不能被当作函数式语言。需要介入赋值的正确决断必须处理好模块化的问题,这超出了本文的范围。Abelson 等人(1985) 的书的第三章是详细分析的好来源。Lisp 表达式可以引用其他表达式,要么直接的要么通过把某种函数应用到参数。每个对象,包括函数,可以动态的构造和操纵;所以我们可以有返回函数的函数,改变函数定义的函数,等等。存储管理是自动进行的。对对象没有类型限制,但程序员可以通过选择适当的标适符来细心的介入(数据)结构。尽管为变量声明关联上数据类型通常被认可为编译型编程语言的重要特征,它在解释性环境没有多大用处。它的主要用处在于使编译器可以优化存储和执行,并对程序做一致性检查。解释器动态的建立数据结构,所有优化并不重要。自动的一致性检查可能使需要的,但是因为程序员在程序失败的时候有权访问所有绑定的来源和值,错误预防通常不象编译环境中那么关键。它可以被高级的错误检查和更正特征所取代。当然,效验和优化一个已经稳定了的程序仍使非常需要的。

递归和条件表达式合起来是 Lisp 的主要控制结构。这是恰当的,因为它允许容易的和优雅的定义重复的规则的结构。在常规计算机体系上递归的低效一直是对 Lisp 的主要批评之一。尽管这种讨论直到几年前还是有益的,现在在很大程度已经被新的实现技术和专用“Lisp 机器”的出现所克服,Lisp 机器在微程序级别支持表和递归,但是,仍然可以公平的说使用 Lisp 经常会导致“内存饥荒”的程序。

Lisp 是一个交互语言,支持试探风格的程序开发。结构编辑器和复杂的调试工具通常作为它所嵌入的编程环境的一部分。这使编写 Lisp 函数的任务非常容易,而不用管众所周知的那些错综复杂的圆括号。结构编辑器可以帮助圆括号恐惧症患者战胜 Lisp 稀少的语法带来的多数缺点。

很多其他经常引证的缺点(比如,动态作用域,缺乏“块结构”,单一的数据类型,低效的数值操作和计算效率)已经在新方言中以各种方式去除了。

经管很多年过去了,仍然只有不多的讲授用 Lisp 编程的教科书,这种情况最近已经戏剧性的改变了。现在已经有了针对不同方言的、对不同背景的读者广泛的教科书。Siklossy(1976) 的书使最老的一本。它很大程度上限制自身到 Lisp 1.5 已经介入的那些特征。Allan(1978) 仍是一本出色的技术性介绍,使用“M-表示法”使它很大程度上独立于任何特定方言,它还包含了对实现要点的一个可靠的讨论。但是 Allen 的教科书对没有先前的编程经验的学生可能是不可企及的,Touretzki(1984) 和 Hasemer(1984) 写的书明确的针对这种“初学者”。Wilensky (1984) 是更加面向技术的另一个有趣的教科书。这本书有两个版本可获得;一本基于 FranzLisp 而另一本基于 Common Lisp。Winston 和 Horn(1984) 强调 AI 应用。他们的书是 Winston(1984) 的伙伴并且被重写了来确保 Common Lisp 兼容。Tanimoto(1987) 是关于 AI 方法论的一个新近的教科书。同很多这种教科书一样它包含一个优秀的、但必须是简要的对 Lisp 的介绍。最后, Charniak 等(1980)写一本教科书,基于 UCI Lisp,对于 AI 编程技术给予高级的对待,它的一个新版不久就会出版。Abelson、Sussman 和 Sussman(1985) 仍是关于 Scheme 的最重要的教科书。Friedman 和 Felleisen(1986) 和 Eisenberg(1987) 提供一个要求更少的介绍。Dybvig(1987),ChezScheme 的开发者,写带有一些更充实的例子的一本教科书,在其中他还覆盖了如引擎和语义扩展这样的特征。有一个活跃的 Scheme 用户组通过在 MIT(uucp: Scheme@mc.lcs.mit.edu)的邮件列表交流。

个主工具