语言

作者:Robert Sanders

Unix 最棒的地方在于你可以编写程序来做任何你想做的事情。缺点是你可能每次想完成任何事情都必须编写一个程序。Unix 系统管理员——以及普通的 Linux 用户也是自己的管理员——面临着看似永无止境的小任务流,这些任务对于人手来说过于繁琐,但对于大型编程工作来说又不够频繁。

我编写的大多数程序都只运行一次就被抛弃了。数量明显较少的程序每周运行一次。每天运行的程序数量我一只手就能数得过来。显然,我不能在任何一个程序上花费太多时间。我需要一种易于开发、易于调试和易于扩展的语言。传统的 Unix 编程语言 C 无法提供这些。在本文中,我将介绍一种可以做到这一点的语言。

Scheme 与 Lisp 密切相关,Lisp 这种语言的名字曾经代表“LISt Processing”(列表处理)。Lisp 最早于 1958 年问世;不幸的是,从那时起它并没有成为商业上的明星。事实上,普遍的看法是 Lisp 及其姊妹语言 Scheme 臃肿且速度慢。虽然这在程序员都在用汇编语言编写代码,并用牙齿拨动程序到计算机前面板上的糟糕旧时代可能是真的,但今天,最大化程序员的生产力比最小化机器成本更重要。Scheme 通过为程序员提供一种灵活但安全的语言来实现这一目标,这种语言允许程序员在比 C 更高的层次上操作。

Scheme 的特性

你可能会问,它是如何做到这一点的呢?首先,也是最重要的是,Scheme 提供了自动内存管理。C 程序员必须显式地分配和释放他使用的每个对象。如果程序分配的比释放的多,内存就会泄漏并被浪费。如果程序释放得过于频繁,程序将行为不正确甚至崩溃。由于一个称为“垃圾回收”的过程,Scheme 程序员只需要关心分配。他在需要时分配一个对象,当该对象不再需要时,Scheme 运行时系统会释放它。

Scheme 提供了比 C 更丰富的数据类型选择。虽然 C 程序员只有数字、字符、数组和指针可供选择,但 Scheme 程序员可以使用数字、字符、数组、字符串、列表、关联列表、函数、闭包、端口和布尔值。此外,Scheme 和 C 在如何处理类型信息方面存在分歧:C 为每个变量分配一个类型,并且每个变量只能保存该类型的值。Scheme 不为变量分配类型,而是用类型标识每个值。这种方法的好处之一是它允许“多态性”,这意味着一个函数可以接受多种类型的参数。例如,您可能有一个函数可以搜索单词或单词列表。

一个可以说次要的问题在使 Scheme 成为如此出色的开发语言中发挥着重要作用:Scheme 可以以解释型和编译型实现形式提供。我对解释型语言的经验表明,使用解释型语言进行开发可以更快地进行原型设计和调试最终产品。在用解释型语言(主要是 Perl 和 Scheme/Lisp)编程两年后,我无法忍受困扰 C 程序员的编辑-编译-运行周期。对于大多数 Scheme 解释器,您可以简单地重新加载您已更改的特定函数定义。您可以从调试器中使用该语言的全部功能,甚至可以修改正在运行的程序!

最后,Scheme 程序通常比其 C 程序更安全、更健壮。Scheme 原语是类型安全的——不像 C 原语那样允许您添加字符串、字符和整数,或将任何数字强制转换为指针然后取消引用它——并且 Scheme 环境提供的错误检查比任何 C 编译器都好得多。

底线

为什么 C 允许这些缺陷存在?Scheme 的便利性是免费的吗?当然不是。

与计算机科学中的大多数事物一样,您有三个选择:快速、廉价和正确(您可以选择两个)。最常见的 Scheme 实现,即解释器,存在速度慢和内存使用略有膨胀的问题。Scheme 编译器生成速度更快的代码,但代价是可执行文件更大,并且错误检查减少(或完全移除)。您选择哪两个属性取决于您需要哪两个属性。大多数程序不需要快如闪电,但如果需要,您可以使 Scheme 变得快速。

另一个缺点直接源于 C 的压倒性普及。大多数外部库和系统接口都以 C 链接库的形式提供。Scheme 用户几乎无法访问此类库。我最喜欢的 Scheme 实现的解决方案是其外部函数接口 (FFI)。FFI 允许 Scheme 程序访问以另一种语言编写的变量和函数。这是一个简短的 Scheme 程序,它使用 Bigloo(Scheme 的一个版本)提供的 FFI 来访问 C 函数“printf”和全局系统错误变量“errno”

(module ffi-example
        (foreign (int errno "errno")
                 (int printf
                       (string . foreign)
                       "printf"))
        (main show-errno))
 (define (show-errno argv)
   (printf "The value of errno is %d" errno)
   (newline))

Scheme 可以通过类似的指令允许 C 使用其函数。借助合适的 FFI,Scheme 和 C 程序可以像两个 C 程序一样自由地共享数据和接口。

即使是最慢的 Scheme 解释器也足以满足我大多数日常程序的速度要求。大多数 Unix 程序的大部分时间都花在等待 I/O 完成上,我的程序也不例外。少数必须尽可能具有计算效率的程序通过使用 Scheme 编译器获得了可观的速度提升。在某些情况下,Bigloo 编译器在最大优化下编译的程序与使用“gcc -O2”编译的 C 等效程序运行速度完全相同。下面提供了一个简单的示例。(请参阅表格和两个程序列表)。

time       language
0.72    gcc -O2
0.72    Bigloo -unsafe
1.03    Bigloo
2.92    SCM/compiled
3.00    Scheme->C
79.04   Scheme48
90.30   SCM
91.76   Perl5
109.04  GNU awk
174.36  Perl4

Bigloo 版本

(module optest
       (main main))
(define (main argv)
  (let ((b (string->integer (cadr argv)))
        (j 0))
    (do ((i 1 (+ i 1)))
        ((> i b))
 (if (even? i)
          (set! j (+ j 1))
          (set! j (- j 1))))
    (display j)
    (newline)))
C 版本
int
main(int argc, char *argv[])
{
  int i = 0, j = 0, b;
  b = atoi(argv[1]);
  while (i++ < b) {
    i % 2 ? j++ : j-;
  }
  printf("%d\n", j);
  return(j);
}
Scheme 惯用法

既然我已经说服您 Scheme 并不太昂贵,我想向您介绍 Scheme 编程。这方面的最佳参考是“Scheme 算法语言修订版修订版修订版报告”(R4RS),它是原始 Scheme 语言定义的曾曾孙。1990 年的 Scheme IEEE 标准编号为 1178。但是,我将尝试通过几个示例来展示 Scheme 编程的乐趣。

首先,让我解释一下 Scheme 背离了计算机语言的审美规范。与 C 或 Fortran 程序(组织为一系列语句,通常每行一个语句)截然不同,Scheme 程序由一系列带括号的列表“S-表达式”组成。每个 S-表达式可以定义一个新函数或变量、调用一个函数,或者只是一个文字数据列表。这是 Scheme 的部分天才之处:程序代码和数据几乎无法区分。S-表达式可以包含其他 S-表达式(其中可能包含其他 S-表达式,等等)。这意味着 Scheme 等效的语句可以包含其他语句,并且 Scheme 列表可以包含其他列表。要真正理解 Scheme,您必须习惯这种递归关系。

与大多数面向语句的语言不同,Scheme 没有运算符。所有操作都由函数或特殊形式处理,两者都表面上以 S-表达式的形式存在。C 的“+”运算符在 Scheme 中以“+”函数的形式出现。函数通过将其名称放在代码 S-表达式的开头来调用。例如,(+ 2 2) 产生数字 4。S-表达式 (display (+ 2 2)) 在标准输出上打印数字 4。其他一些 S-表达式

(define some-variable 12)
(define (some-function argument)
   (display argument))
(- some-variable 1)
(display (* some-variable 2))

大多数 Scheme 新手一开始不喜欢括号,但后来会喜欢上它们。Scheme 的语法比基于状态的语言更规则,更利于语言敏感编辑,并且更容易使用宏进行操作。(宏超出了本文的范围。)

条件表达式

Scheme 提供了熟悉的“if”表达式,用于代码的条件执行。C 的 if 语句和 Scheme 的 if 语句之间的一个区别是 Scheme 的“if”语句返回一个值。事实上,所有 Scheme 语句都返回值。Lisp 的这个特性,加上 Lisp 系统据称的低效率,导致一些智者评论说,“Lisp 程序员知道一切的价值,却不知道任何事物的成本。”

如果传递给它的数字不是 1,此函数会打印“s”。

(define (plural-print-s num)
  (display (if (eq? num 1) "" "s")))

另一个条件语句是“cond”形式;“cond”不是简单的二选一选择,而是评估几个测试中的每一个,并执行第一个评估为 true 的测试的相应表达式。

(define (print-type object)
  (cond ((number? object)
         (display "number"))
        ((string? object)
         (display "string"))
        ((list? object)
         (display "list"))
        (else
         (display "I don't know that type"))))
递归和迭代

大多数 C 程序员都会熟悉用于重复的“for”和“while”迭代循环。递归在 C 世界中鲜为人知。Scheme 提供了几种非常强大的重复方法,包括迭代形式和递归形式。

此递归函数打印一个数字列表,每个数字递增 1。

(define (print-list+1 arglist)
  (if (pair? arglist)
      (begin
      (display (+ 1 (car arglist)))
      (newline)
      (print-list+1 (cdr arglist)))))

(car 和 cdr 是分别返回列表的第一个元素和减去其第一个元素的列表的函数。)

认识到对列表的每个元素应用操作是一种常见操作,Scheme 提供了“map”函数。这是使用“map”编写的相同程序

(define (print-list+1 arglist)
  (map (lambda (arg) (display (+ 1 arg)) (newline))
      arglist))

“lambda”形式类似于函数定义,但生成的函数没有名称。虽然此函数可能无法按名称调用,但它可以传递给将函数作为参数的其他函数(还感到困惑吗?)。在本例中,“map”将其第一个参数作为函数。然后,它将该函数应用于其第二个参数的每个元素,第二个参数必须是列表。

Scheme 还提供了“do”循环,其功能几乎与 C 的“for”循环相同。

Scheme 实现

由于其简单性和简单的语法,Scheme 已成为语言实现者的最爱,因此,有数十种 Scheme 实现可免费使用。这些可以分为两类:解释器和编译器。(嗯,更正确地说,编译器通常包括编译器和解释器。)

我最喜欢的编译器是 Bigloo。Bigloo 由法国 INRIA 的 Manuel Serrano 编写,将 Scheme 代码编译为高效的 C 代码,然后由 gcc 编译为可执行代码。由于这种方法,Bigloo 既具有可移植性,又可以接受系统 C 编译器的改进。如前所述,Bigloo 编译的程序通常在 C 等效程序的几个百分点内运行。Manuel 目前正处于博士论文的撰写过程中,可以通过 Manuel.Serrano@inria.fr 与他联系。

其他编译器包括广泛使用的 Scheme->C,它由 DEC 西部研究实验室的 Joel Bartlett(仍然可以通过 bartlett@decwrl.dec.com 联系到他)制作。Scheme->C 编译的程序在简单任务中的速度不如 Bigloo 编译的程序,但 Scheme->C 具有更高级的垃圾回收器。对于具有大型数据集的程序,Scheme->C 可能是更明智的选择。

Chez Scheme 也可用于 Linux。Chez 是由 R. Kent Dybvig (dybvig@cs.indiana.edu ) 编写的久负盛名的编译器,他是事实上的 Scheme 标准 (R4RS) 的作者之一。Chez Scheme 不是免费的。

最近几个月,一些 Scheme 解释器越来越受欢迎。最近最热门的产品之一是 STk,它是一个解释器,它使 John Ousterhout 的 Tk 易于使用的窗口小部件集可以通过 Scheme 的面向对象方言使用。STk 不是解释器领域中性能最好的,但它轻松击败了 Ousterhout 的 Tcl。

Aubrey Jaffer 的 SCM 是最成熟的 Scheme 解释器之一,它具有体积小、速度快和不断增长的扩展库。这些模块包括 POSIX 接口、套接字 I/O 和 curses 屏幕管理库。SCM 的作者还在一个名为 SLIB 的包中维护着一个有用的 Scheme 函数库。我在我的几个代码示例中使用了 SLIB。作者 Aubrey Jaffer 可以通过 jaffer@ai.mit.edui 联系到他。

自由软件基金会最近开始努力为其产品提供标准的脚本和应用程序扩展语言。这种语言被称为 GUILE,原因是一些首字母缩略词,我现在想不起来了,它将基于 SCM 解释器。要了解有关 GUILE 的更多信息,请发送邮件至 gel-request@cygnus.com(这不是拼写错误!)。

最后,MIT(Scheme 的诞生地)的一群 Scheme 爱好者开展了一个雄心勃勃的项目,使 Scheme 在 Unix 脚本编写方面与 Bourne 和 Korn shell 一样实用。他们的 Scheme shell (scsh) 将卓越的 Scheme 语言与 Unix shell 强大的基于进程/管道的数据流机制相结合。

程序示例

以下是一些程序示例,让您感受一下 Scheme 编程。

程序 1

程序 2

列表 3

程序 1 是标准的斐波那契算法,程序 2 是标准的递归阶乘。这些应该在任何 Scheme 实现下都能工作。列表 3 显示了 SCM 解释器和 SLIB(其库包)的经过验证的 Unix->DOS 文件转换实用程序的实现。它读取一个以换行符结尾的行文件,并输出一个行以回车符和换行符结尾的文件。特别注意函数 chomp 的定义;此函数将字符串拆分为字符列表,过滤掉所有不需要的字符,然后将列表折叠回字符串。列表 4 显示了与 C 程序员编写的更接近的 chomp 定义。请注意复杂性的显着差异。

列表 4

列表 5

列表 5 是一个程序,它接受命令行上的文件列表,并将它们排列成磁盘大小的组。对于那些通过软盘安装 Linux 的人来说,这个过程应该很熟悉。该程序说明了 Scheme 列表操作过程的强大功能。map 和 for-each 都为列表的每个成员执行一个函数。find-if 返回列表中与指定条件匹配的第一个元素。这些和其他列表技术通常在 C 中使用繁琐、容易出错的循环来实现。

结束语

每种语言都在程序员的工具包中占有一席之地。我不会将 Scheme 用于每项任务——大多数时候我使用 Perl,有时我甚至使用 C。但是,与几乎任何其他语言相比,Scheme 可以让我在更短的时间内、以更少的精力和更少的痛苦编写无错误的程序。它为我提供了许多设施,如果使用其他一些语言,我必须自己编写这些设施,并且与一些其他非常高级的语言不同,它可以编译为速度极快的本机代码。

为您的 Linux Box 获取 Scheme

我维护着一个预编译用于 Linux 的 Scheme 解释器和编译器的存档。要检索其中一个,请 ftp 到 ftp.mindspring.com 并查找名为 /users/rsanders/lang 的目录。您会找到以下文件

  • bigloo-bin.tar.gz Bigloo 编译器版本

  • bigloo-elf-bin.tar.gz 带有 ELF 共享库的 Bigloo

  • scheme2c-bin.tar.gz DEC 的 Scheme->C 编译器

  • scheme2c-elf-bin.tar.gz 带有 ELF 共享库的 Scheme->C

  • scm-bin.tar.gz Aubrey Jaffer 的 SCM 解释器

  • slib.tar.gz Jaffer 的 SLIB 库

  • stk-bin.tar.gz 兼容 Tk 的 Scheme 解释器

如果您的系统能够编译和运行 ELF 二进制文件,那么我建议您使用包含 ELF 共享库的软件包。使用这些共享库可以将应用程序启动时间和大小最多减少 180 KB。

参考书目

Scheme 算法语言修订版修订版修订版报告 (R4RS) - William Clinger、Jonathan Rees 等人。Postscript 和 DVI 版本可通过匿名 FTP 从 swiss-ftp.ai.mit.edu 的 /archive/scheme-reports 获得。HTML 格式也可从 swiss-ftp.ai.mit.edu 以 /archive/scm/HTML/r4rs* 获得。Usenet 新闻组 comp.lang.scheme 和 comp.lang.lisp。入门书籍(来自 comp.lang.scheme FAQ)

  1. Daniel P. Friedman 和 M. Felleisen。The Little LISPerMIT Press (Cambridge, MA), 第 3 次印刷, 1989. ISBN 0-262-56038-0。Science Research Associates (Chicago), 第 3 版, 1989. 206 页。

    非常适合快速入门。使用 Scheme 而不是 Common Lisp。(本书使用 Scheme 的一种方言,并带有关于翻译成 Scheme 或 Common Lisp 的脚注。由于复杂性,脚注不允许非专家将 Common Lisp 用于高级章节。)

  2. Brian Harvey 和 Matthew WrightSimply Scheme: Introducing Computer ScienceMIT Press, Cambridge,MA, 1994. 583 页。ISBN 0-262-08226-8。49.95 美元。

    本书非常适合以前几乎没有接触过编程的学生。本书旨在在 SICP 之前使用(作者称其为 SICP 的“前传”),并通过让学生远离可能令人困惑的技术细节,使 Scheme 变得有趣。与 Pascal 或 C 不同,重点是思想,而不是晦涩的语法问题和任意的风格规则。因发现 SICP 太具挑战性而避开使用 Scheme 的高中应该考虑改用本书。

    本文逐步温和地向学生介绍了 Scheme 编程的一些关键概念。它从函数和函数组合开始,然后继续介绍作为数据(一等函数)的函数和编写程序的程序(高阶函数)的概念。由于该语言的复杂性被隐藏起来,学生可以比其他教材更早地参与到该语言的一些更有趣和有趣的方面。然后,本书逐步介绍 lambda、递归、数据抽象和过程抽象等更复杂的概念,并以顺序技术结束,但仔细关注学生通常认为困难的主题。仅递归就有五章!大多数章节末尾还有一个陷阱部分,以帮助学生识别和避免常见错误。本书使用多个程序作为示例,包括井字棋程序、模式匹配器、微型电子表格和简单的数据库程序。程序的源代码可以通过匿名 ftp 从 anarres.cs.berkeley.edu:/pub/scheme/ 获得,或者从出版商处以 10 美元的价格购买 IBM 或 Macintosh 软盘。

  3. Harold Abelson 和 Gerald Jay Sussman,与 Julie Sussman。Structure and Interpretation of Computer ProgramsMIT Press (Cambridge, MA) 和 McGraw-Hill (New York), 1985. 542 页。ISBN 0-262-01077-1 55 美元。

    教师手册也可从 MIT Press 获得(ISBN 0-262-51046-4 20 美元),其中不包含练习的解答,但包含有关使用本书进行教学的提示。

    从入门开始,但迅速深入到强大的 Lisp 特有结构,例如使用闭包和引擎、构建解释器、编译器和面向对象的系统。通常以其首字母缩略词 SICP 指代,发音为“Sick-Pee”。这是使用 Scheme 教授程序设计的经典教材,每个人都应该至少阅读一遍。MIT 问题集可从存储库获得,Gustavus Adolphus College 的材料可从 ftp.gac.edu:/pub/SICP/ 获得。

  4. George Springer 和 Daniel P. FriedmanScheme and the Art of ProgrammingMIT Press 和 McGraw Hill, 1990, 596 页。ISBN 0-262-19288-8, 50 美元。

    介绍 Scheme 编程的基本概念。还涉及面向对象编程、协程和延续。提供了许多示例。比 SICP 更注重教授 Scheme,可以看作是 SICP 的替代品。来自章节的源代码可从 ftp.cs.indiana.edu:/pub/scheme-repository/lit/sap/ 获得

Robert Sanders (rsanders@mindspring.com) 在 MindSpring Enterprises(一家位于亚特兰大的互联网服务提供商)担任高级工程师。自从他摆脱了 dosemu 作者的角色以来,他就一直努力研究计算机语言及其实现。他喜欢 Linux。

加载 Disqus 评论