语言描述
1. Scheme概论
本章概述了Scheme的语义。此概述的目的在于充分解释语言的基本概念,以帮助理解本报告的后面的章节,这些章节以参考手册的形式被组织起来。因此,本概述不是本语言的完整介绍,在某些方面也不够精确和规范。
像Algol语言一样,Scheme是一种静态作用域的程序设计语言。对变量的每一次使用都对应于该变量在词法上的一个明显的绑定。
Scheme中采用的是隐式类型而非显式类型13。类型与对象(也称值)相关联,而非与变量相关联。(一些作者将隐式类型的语言称为弱类型或动态类型的语言。)其它采用隐式类型的语言有Python, Ruby, Smalltalk和Lisp的其他方言。采用显式类型的语言(有时被称为强类型或静态类型的语言)包括Algol 60, C, C#, Java, Haskell和ML。
在Scheme计算过程中创建的所有对象,包括过程和继续(continuation),都拥有无限的生存期(extent)。Scheme对象从不被销毁。Scheme的实现(通常!)不会耗尽空间的原因是,如果它们能证明某个对象无论如何都不会与未来的任何计算发生关联,它们就可以回收该对象占据的空间。其他允许多数对象拥有无限生存期的语言包括C#, Java, Haskell, 大部分Lisp方言, ML, Python, Ruby和Smalltalk。
Scheme的实现必须支持严格尾递归。这一机制允许迭代计算在常量空间内执行,即便该迭代计算在语法上是用递归过程描述的。借助严格尾递归的实现,迭代就可以用普通的过程调用机制来表示,这样一来,专用的迭代结构就只剩下语法糖衣的用途了。
Scheme是最早支持把过程在本质上当作对象的语言之一。过程可以动态创建,可以存储于数据结构中,可以作为过程的结果返回,等等。其他拥有这些特性的语言包括Common Lisp, Haskell, ML, Ruby和Smalltalk。
在大多数其他语言中只在幕后起作用的继续,在Scheme中也拥有“第一级”状态,这是Scheme的一个独树一帜的特征。第一级继续可用于实现大量不同的高级控制结构,如非局部退出(non-local exits)、回溯(backtracking)和协作程序(coroutine)等。
在Scheme中,过程的参数表达式会在过程获得控制权之前被求值,无论这个过程需不需要这个值。C, C#, Common Lisp, Python, Ruby和Smalltalk是另外几个总在调用过程之前对参数表达式进行求值的语言。这和Haskell惰性求值(lazy-evaluation)的语义以及Algol 60按名求值(call-by-name)的语义不同,在这些语义中,参数表达式只有在过程需要这个值的时候才会被求值。
Scheme的算术模型被设计为尽量独立于计算机内数值的特定表示方式。此外,他还区分精确数和非精确数对象:本质上,一个精确数对象精确地等价于一个数,一个非精确数对象是一个涉及到近似或其它误差的计算结果。
1.1. 基本类型
Scheme程序操作对象(object),有时也被成为值(value)。Scheme对象被组织为叫做类型(type)的值的集合。本节将给你Scheme语言十分重要的类型的一个概述。更多的类型将在后面章节进行描述。
注意:由于Scheme采用隐式类型,所以本报告中术语类型的使用与其它语言文本中本术语的使用不同,尤其是与那些显式语言中的不同。布尔(Booleans) 布尔类型是一个真值,可以是真或假。在Scheme中,对象“假”被写作#f。对象“真”被写作#t。然而,在大部分需要一个真值的情况下,凡是不同于#f的对象被看作真。
数值(Numbers) Scheme广泛支持各类数值数据结构,包括任意精度的整数,有理数,复数和各种类型的非精确数。第3章给了Scheme数值塔结构的概述。
字符(Characters) Scheme字符多半等价于一个文本字符。更精确地说,它们同构与Unicode字符编码标准的标量值(scalar values)。
字符串(Strings) 字符串是确定长度字符的有限序列,因此它代表任意的Unicode文本。
符号(Symbols) 符号是一个表示为字符串的对象,即它的名字。不同于字符串,两个名字拼写一样的符号永远无法区分。符号在许多应用中十分有用;比如:它们可以像其它语言中枚举值那样使用。
点对(Pairs)和表(lists) 一个点对是两个元素的数据结构。点对最常用的用法是(逐一串联地)表示表,在这种表结构中,第一个元素(即“car”)表示表的第一个元素,第二个元素(即“cdr”)表示表的剩余部分。Scheme还有一个著名的空表,它是表中一串点对的最后一个cdr。
向量(Vectors) 像表一样,向量是任意对象有限序列的线性数据结构。然而,表的元素循环地通过链式的点对进行访问,向量的元素通过整数索引进行标识。所以,比起表,向量更适合做元素的随机访问。
过程(Procedures) 在Scheme中过程是值。
1.2. 表达式(Expressions)
Scheme代码中最重要的元素是表达式。表达式可以被计算(evaluated),产生一个值(value)。(实际上,是任意数量的值—参见5.8节。)最基本的表达式就是字面的表达式:
#t ⇒ #t
23 ⇒ 23
这表明表达式#t的结果就是#t,也就是“真”的值,同时,表达式23计算得出一个表示数字23的对象。
复合表达式通过在子表达式周围放置小括号来组合。第一个子表达式表示一个运算;剩余的子表达式是运算的操作数:
(+ 23 42) ⇒ 65
(+ 14 (* 23 42)) ⇒ 980
上面例子中的第一个例子,+是内置的表示加法的运算的名字,23和42是操作数。表达式(+ 23 42)读作“23和42的和”。复合表达式可以被嵌套—第二个例子读作“14和23和42的积的和”。
正如这些例子所展示的,Scheme中的复合表达式使用相同的前缀表示法(prefix notation)书写。所以,括号在表达这种结构的时候是必要的。所以,在数学表示和大部分语言中允许的“多余的”括号在Scheme中是不被允许的。
正如其它许多语言,空白符(包括换行符)在表达式中分隔子表达式的时候不是很重要,它也可以用来表示结构。
1.3. 变量(variable)和绑定(binding)
Scheme允许标识符(identifier)代表一个包含值得位置。这些标识符叫做变量。在许多情况下,尤其是这个位置的值在创建之后再也不被修改的时候,认为变量直接代表这个值是非常有用的。
(let ((x 23)
(y 42))
(+ x y)) ⇒ 65
在这种情况下,以let开头的表达式是一种绑定结构。跟在let之后的括号结构列出了和表达式一起的变量:和23一起的变量x,以及和42一起的变量y。let表达式将x绑定到23,将y绑定到42。这些绑定在let表达式的内部(body)是有效的,如上面的(+ x y),也仅仅在那是有效的。
1.4. 定义
let表达式绑定的变量是局部的(local),因为绑定只在let的内部可见。Scheme同样允许创建标识符的顶层绑定,方法如下:
(define x 23)
(define y 42)
(+ x y) ⇒ 65
(这些实际上是在顶层程序或库的内部的“顶层”;参见下面的1.12节。)
开始的两个括号结构是定义,它们创造了顶层的绑定,绑定x到23,绑定y到42。定义不是表达式,它不能出现在所有需要一个表达式出现的地方。而且,定义没有值。
绑定遵从程序的词法结构:当存在相同名字的绑定的时候,一个变量参考离它最近的绑定,从它出现的地方开始,并且以从内而外的方式进行,如果在沿途没有发现局部绑定的话就使用顶层绑定:
(define x 23)
(define y 42)
(let ((y 43))
(+ x y)) ⇒ 66
(let ((y 43))
(let ((y 44))
(+ x y))) ⇒ 67
1.5. 形式(Forms)
尽管定义不是表达式,但是复合表达式和定义有相同的语法形式:
(define x 23)
(* x 2)
尽管第一行包含一个定义,第二行包含一个表达式,但它们的不同依赖于define和*的绑定。在纯粹的语法层次上,两者都是形式,形式是Scheme程序一个语法部分的名字。特别地,23是形式(define x 23)的一个子形式(subform)。
1.6. 过程
定义也可以被用作定义一个过程:
(define (f x)
(+ x 42))
(f 23) ⇒ 65
简单地说,过程是一个表达式对象的抽象。在上面这个例子中,第一个定义定义了一个叫f的过程。(注意f x周围的括号,这表示这是一个过程的定义。)表达式(f 23)是一个过程调用,大概意思是“将x绑定到23计算(+ x 42)(过程的内部)的值”。
由于过程是一个对象,所以它们可以被传递给其它过程:
(define (f x)
(+ x 42))
(define (g p x)
(p x))
(g f 23) ⇒ 65
在上面这个例子中,g的内部被视为p绑定到f,x绑定到23,这等价于(f 23),最终结果是65。
实际上,Scheme许多预定义的操作不是通过语法提供的,而是值是过程的变量。比如,在其它语言中受到特殊对待的+操作,在Scheme中只是一个普通的标识符,这个标识符绑定到一过程,这个过程将数字对象加起来。*和许多其它的操作也是一样的:
(define (h op x y)
(op x y))
(h + 23 42) ⇒ 65
(h * 23 42) ⇒ 966
过程定义不是定义过程的唯一方法。一个lambda表达式可以在不指定一个名字的情况下以对象的形式生成一个过程:
((lambda (x) (+ x 42)) 23) ⇒ 65
这个例子中的整个表达式是一个过程调用;(lambda (x) (+ x 42)),相当于一个输入一个数字并加上42的过程。
1.7. 过程调用和语法关键词(syntactic keywords)
尽管(+ 23 42), (f 23)和((lambda (x) (+ x 42)) 23)都是过程调用的例子,但lambda和let表达式不是。这时因为尽管let是一个标识符,但它不是一个变量,而是一个语法关键词。以一个语法关键词作为第一个子表达式的形式遵从关键词决定的特殊规则。define标识符是一个定义,也是一个语法关键词。因此,定义也不是一个过程调用。
lambda关键词的规则规定第一个子形式是一个参数的表,其余的子形式是过程的内部。在let表达式中,第一个子形式是一个绑定规格的表,其余子形式构成表达式的内部。
通过在形式的第一个位置查找语法关键词的方法,过程调用通常可以和这些特殊形式区别开来:如果第一个位置不包含一个语法关键词,则这个表达式是一个过程调用。(所谓的标识符的宏(identifier macros)允许创建另外的特殊形式,但相当不常见。)Scheme语法关键词的集合是非常小的,这通常使这项任务变得相当简单。可是,创建新的语法关键词的绑定也是有可能的,见下面的1.9节。
1.8. 赋值(Assignment)
Scheme变量通过定义或let或lambda表达式进行的绑定不是直接绑定在各自绑定时指定的对象上,而是包含这些对象的位置上。这些位置的内容随后可通过赋值破坏性地改写:
(let ((x 23))
(set! x 42)
x) ⇒ 42
在这种情况下,let的内部包括两个表达式,这两个表达式被顺序地求值,最后一个表达式的值会成为整个let表达式的值。表达式(set! x 42)是一个赋值,表示“用42代替x所指向位置的对象”。因此,x以前的值,23,被42代替了。
1.9. 衍生形式(Derived forms)和宏(macros)
在本报告中,许多特殊的形式可被转换成更基本的特殊形式。比如,一个let表达式可以被转换成一个过程调用和一个lambda表达式。下面两个表达式是等价的:
(let ((x 23)
(y 42))
(+ x y)) ⇒ 65
((lambda (x y) (+ x y)) 23 42) ⇒ 65
像let表达式这样的特殊形式叫做衍生形式,因为它们的语义可以通过语法转换衍生自其它类型的形式。一些过程定义也是衍生形式。下面的两个定义是等价的:
(define (f x)
(+ x 42))
(define f
(lambda (x)
(+ x 42)))
在Scheme中,一个程序通过绑定语法关键词到宏以定义它自己的衍生形式是可能的:
(define-syntax def
(syntax-rules ()
((def f (p ...) body)
(define (f p ...)
body))))
(def f (x)
(+ x 42))
define-syntax结构指定一个符合(def f (p ...) body)模式的括号结构,其中f,p和body是模式变量,被转换成(define (f p ...) body)。因此,这个例子中的def形式将被转换成下面的语句:
(define (f x)
(+ x 42))
创建新的语法关键词的能力使得Scheme异常灵活和负有表现力,允许许多内建在其它语言的特性在Scheme中以衍生形式出现。
1.10. 语法数据(Syntactic data)和数据值(datum values)
Scheme对象的一个子集叫做数据值。这些包括布尔,数据对象,字符,符号,和字符串还有表和向量,它们的元素是数据。每一个数据值都可以以语法数据的形式被表现成字面形式,语法数据可以在不丢失信息的情况下导出和读入。一个数据值可以被表示成多个不同的语法数据。而且,每一个数据值可以被平凡地转换成一个一个字面表达式,这种转换通过在程序中加上一个'在其对应的语法数据前:
'23 ⇒ 23
'#t ⇒ #t
'foo ⇒ foo
'(1 2 3) ⇒ (1 2 3)
'#(1 2 3) ⇒ #(1 2 3)
在数字对象和布尔中,前一个例子中的'是不必要的。语法数据foo表示一个名字是“foo”的符号,且'foo是一个符号作为其值对的字面表达式。(1 2 3)是一个元素是1,2和3的表的语法数据,且'(1 2 3)是一个值是表的字面表达式。同样,#(1 2 3)是一个元素是1,2和3的向量,且'#(1 2 3)是对应的字面量。
语法数据是Scheme形式的超集。因此,数据可以被用作以数据对象的形式表示Scheme形式。特别地,符号可以被用作表示标识符。
'(+ 23 42) ⇒ (+ 23 42)
'(define (f x) (+ x 42))
⇒ (define (f x) (+ x 42))
这方便了操作Scheme源代码程序的编写,尤其是解释和程序转换。
1.11. 继续(Continuations)
在Scheme表达式被求值的时候,总有一个继续在等待这个结果。继续表示计算的一整个(默认的)未来。比如,不正式地说,表达式
(+ 1 3)
中3的继续给它加上了1。一般地,这些普遍存在的继续是隐藏在背后的,程序员不需要对它们考虑太多。可是,偶尔,一个程序员需要显示地处理继续。过程call-with-current-continuation(见11.15节)允许Scheme程序员通过创建一个接管当前继续的过程来处理这些。过程call-with-current-continuation传入一个过程,并以一个逃逸过程作为参数,立刻调用这个过程。随后,这个逃逸过程可以用一个参数调用,这个参数会成为call-with-current-continuation的结果。也就是说,这个逃逸过程放弃了它自己的继续,恢复了call-with-current-continuation调用的继续。
在下面这个例子中,一个代表加1到其参数的逃逸过程被绑定到escape上,且然后以参数3被调用。escape调用的继续被放弃,取而代之的是3被传递给加1这个继续:
(+ 1 (call-with-current-continuation
(lambda (escape)
(+ 2 (escape 3)))))
⇒ 4
一个逃逸过程有无限的生存期:它可以在它捕获的继续被调用之后被调用,且它可以被调用多次。这使得call-with-current-continuation相对于特定的非本地跳转,如其它语言的异常,显得异常强大。
1.12. 库
Scheme代码可以被组织在叫做库的组件中。每个库包含定义和表达式。它可以从其它的库中导入定义,也可以向其它库中导出定义。
下面叫做(hello)的库导出一个叫做hello-world的定义,且导入基础库(base library)(见第11章)和简单I/O库(simple I/O library)(见库的第8.3小节)。hello-world导出的是一个在单独的行上显示Hello World的过程。
(library (hello)
(export hello-world)
(import (rnrs base)
(rnrs io simple))
(define (hello-world)
(display "Hello World")
(newline)))
1.13. 顶层程序
一个Scheme程序被一个顶层程序调用。像库一样,一个顶层程序包括导入,定义和表达式,且指定一个执行的入口。因此,一个顶层程序通过它导入的库的传递闭包定义了一个Scheme程序。
下面的程序通过来自(rnrs programs (6))库(见库的第10章)的command-line过程从命令行获得第一个参数。它然后用open-file-input-port(见库的第8.2小节)打开一个文件,产生一个端口(port),也就是作为一个数据源的文件的一个连接,并调用get-bytes-all过程以获得文件的二进制数据。程序然后使用put-bytes输出文件的内容到标准输出:
(译注:以下的示例程序已根据勘误表进行了修正。)
#!r6rs
(import (rnrs base)
(rnrs io ports)
(rnrs programs))
(let ((p (standard-output-port)))
(put-bytevector p
(call-with-port
(open-file-input-port
(cadr (command-line)))
get-bytevector-all))
(close-port p))