5. 语义概念
5.1. 程序和库
一个Scheme程序由一个顶层程序以及一系列的库组成,每个库通过显式地指定导入和导出定义了和其它程序相联系的程序的一部分。一个库由一系列的导入导出说明和程序体组成,程序体由定义和表达式组成。一个顶层程序和一个库差不多,但是没有导出说明。第7章和第8章分别描述了库和顶层程序的语法和语义。第11章描述了定义了通常和Scheme一起的结构的一个基本库。一个单独的报告1描述了Scheme系统提供的各种标准库。
基本库和其它标准库的划分是根据使用而不是结构。尤其那些通常被编译器或运行时系统实现为“原语(primitives)”而不是根据其它标准调用或句法形式的功能不是基本库的一部分,而是被定义在单独的库中。例子包括定长数和浮点数库,异常和条件库,以及用于记录的库。
5.2. 变量,关键词和作用域
在一个库或顶层程序的内部,一个标识符可以命名一种语法,或者它可以命名一个值可以被存储的位置。命名一种语法的标识符叫做关键词,或句法关键词,也可以说是关键词被绑定到那个语法(或者说是一个句法抽象,也就是一个将语法转换成更基本的形式的转换器;见7.2小节)。命名一个位置的标识符叫做变量,也可以说是变量被绑定到那个位置。在一个顶层程序或一个库的每一个位置,一个特定的固定集合的标识符被绑定。这些标识符的集合,也就是可见绑定(visible bindings)的集合,被称为影响该位置的环境(environment)。
特定的形式被用作创建语法抽象并绑定关键词到用于这些新的语法抽象的转换,同时其它的形式创建新的位置并将变量绑定到那些位置。这些表达式被统称为绑定结构(binding constructs)。一些绑定结构使用定义的形式,其它的是表达式。除了导出的库绑定是一个例外,一个定义创建的绑定只在这个定义出现的内部可见,例如,库,顶层程序或lambda表达式的内部。导出的库绑定在导入这些绑定的库和程序的内部是可见的(见第7章)。
绑定变量的表达式包括来自基础库的lambda,let,let*,letrec, letrec*, let-values和let*-values形式(见11.4.2和11.4.6小节)。在这些当中,lambda是最基本的。出现在这样的表达式内部,或出现在库或顶层程序内部的变量定义被当作一系列的letrec*对待。另外,在库的内部,从库中导出的变量可以被导入的库或顶层程序引用。
绑定关键词的表达式包括let-syntax和letrec-syntax形式(见11.18小节)。一个define形式(见11.2.1小节)是一个创建变量绑定(见11.2小节)的定义,同时一个define-syntax形式是一个创建关键词绑定(见11.2.2小节)的定义。
Scheme是一个拥有块结构的静态作用域的语言。顶层程序或库的内部的每一个地方,一个标识符被绑定到代码的作用域,在这个作用域中绑定是可见的。这个作用域被特定的建立绑定的绑定结构决定;比如,如果一个绑定通过lambda表达式建立,那么作用域是整个lambda表达式。每当标识符引用标识符的绑定的时候,它总是引用包含这次使用的最内层绑定。如果找不到作用域包含该引用的标识符绑定,它可以引用库或顶层程序最上面的定义或导入建立的绑定(见第7章)。如果找不到标识符的绑定,它就被称为未绑定的。
5.3. 异常情况
在本报告中各种异常情况是有区别的,包括违反语法,违反过程调用,违反实现限制和环境中的异常情况。当实现检测到一个异常情况时,一个异常被抛出,这意味着一个被称为当前异常处理程序(current exception handler)的特殊过程被调用。一个程序也可以抛出一个异常且覆盖当前异常处理程序;见库的第7.1小节。
当一个异常被抛出时,会提供一个描述异常情况性质的对象。本报告使用库的第7.2小节描述的条件系统来描述异常情况,通过条件类型来区分它们。
如果异常处理程序采取适当动作的话,一些异常情况允许程序继续运行。对应的异常被称为是可继续的(continuable)。对于本报告描述的大部分异常情况,可移植的程序不应该依赖在异常被检测到的地方异常是可继续的。在那些异常中,被异常调用的异常处理程序不应该返回。可是,在有些情况下,继续是被允许的,这时处理程序应该返回。见库的第7.1小节。
当实现由于实现限制(implementation restriction)无法正确执行一个正确的程序时,实现必须抛出一个异常。比如,一个不支持无穷大的实现计算一个结果是无穷大的表达式时必须抛出一个条件类型是&implementation-restriction的异常。
一些可能的异常比如缺乏非数和无穷大的表示(见11.7.2小节)是本报告预知的,且实现必须在遭遇到这样的情况的时候抛出条件类型适当的异常。
本报告使用的措辞“一个异常被抛出”等价于“一个异常必须被抛出”。本报告使用措辞“一个条件类型为t的异常”指示跟随异常抛出的对象是一个有特定类型的条件对象。措辞“一个可继续的异常被抛出”指示一个异常情况允许异常处理程序返回。
5.4. 参数检查
本报告中的许多或一个标准库中的部分调用限制它们接受的参数。典型地,一个调用只接受特定的数字和参数类型。许多句法形式同样限制它们的一个或多个子形式计算结果的值。这些限制暗示着程序员和实现共同的责任。尤其,程序员负责确保这些值确实符合本规范描述的限制。实现必须检查本规范描述的限制确实被遵守,在某种程度上来说允许特定的操作成功完成是合理的,可能的,也是必要的。在第6章以及整个报告中,实现的责任会被更加仔细地描述。
注意,一个实现完全检查一个规范的限制并不总是可行的。比如,如果一个操作接受一个有特定属性的过程,检查这些属性通常是不可行的。同样地,一些操作同时接受表和过程,并且这些操作会调用这些过程。因为表可以被来自(rnrs mutable-pairs (6))库(见库的第17章)的过程改变,所以,一个操作开始时还是表的参数可能在操作执行过程中变成一个非表。并且,过程可能逃逸到一个不同的继续中,从而阻止操作进行检查。要求操作检查参数在这样的过程的每一次调用后都是一个表是不现实的。并且,一些接受表参数操作仅仅需要部分地遍历表中的元素;要求实现去遍历表中的其它元素以检查所有的限制被满足可能违反合理的性能假设。由于这些原因,这些检查是程序员的义务而不是实现的检查义务。
当一个实现检测到参数违反限制的时候,它必须以5.6小节描述的符合执行安全性的方法抛出一个条件类型是&assertion的异常。
5.5. 违反语法
一个特定形式的子形式通常需要遵循特定的句法限制。由于形式可能要遵从宏扩展,其可能不会终结,所以,它们是否符合特定的限制的问题通常是无法解答的。
可是,当宏扩展终结时,实现必须检测违反语法的地方。一个语法错误是一个关于库内部,顶层程序内部,或本报告基本库或标准库“语法”入口的错误。此外,试图给一个不可改变的变量(也就是一个库导出的变量;见7.1小节)也被认为是一个语法错误。
如果一个程序中的一个顶层或库形式是句法上不正确的,那么实现必须抛出一个条件类型是&syntax的异常,且顶层程序或库必须不能被允许开始。
5.6. 安全性
导出被本文档描述的标准库被认为是安全库。只从安全库导入的库或顶层程序也被认为是安全的。
正如本文档所定义的,Scheme编程语言在下列的场景下是安全的:一个顶层程序的执行不能严重错误到以至于崩溃或不能以和本报告描述的语义相一致的行为执行,除非一个异常被抛出。
违反实现限制的时候必须抛出一个条件类型是&implementation-restriction的异常,且必须是所有的违规和错误,其可能威胁系统的完整性,使得执行的结果无法和本报告中描述的语义相一致。
上面的安全性属性只有在顶层程序或库被认为是安全的时候被确保。特别地,实现可以提供访问非安全库的方法,这种方法不能保证安全性。
5.7. 布尔值
尽管有单独的布尔值的存在,但是,Scheme的任何值可在条件测试中被当作一个布尔值使用,除了#f,所有的值在测试中被当成真。本报告使用词语“真”表示任意除了#f的Scheme值,用词语“假”表示#f。
5.8. 多个返回值
一个Scheme表达式的结果可以是任意多个有限数量的值。这些值被传递给表达式的继续。
不是所有的继续都接受任意数量的值。比如,一个接受调用参数的继续可以确保精确地接受一个值。传递其它数量的值到这样一个继续的效果是未定义的。11.15小节描述的过程call-with-values使创建接受特定数量返回值的继续成为可能。如果传递给一个call-with-values过程创建的继续的返回值的数量不被这个继续接受,那么一个异常被抛出。11.15小节values过程的描述给了一个关于不同继续接受的值的数量的更全面的描述。
基本库的许多的形式有作为子形式的表达式的序列,这个序列被按顺序求值,但除了最后一个表达式的值,其它所有的值都会被忽略。丢弃这些值得继续接受任意数量的值。
5.9. 未定义(Unspecified)的行为
如果说一个表达式“返回未定义的值”,那么这个表达式必须正确计算而不是抛出一个异常,但是返回值是依赖于实现的;本报告故意不说多少值或什么值被返回。程序员不应该依赖返回值是特定的数量或依赖返回值是特定的值。
5.10. 存储模型
变量和对象,比如点对,向量,字节向量,字符串,哈希表和含蓄地引用位置或位置序列的记录。一个字符串,比如说,包含很多位置因为在一个字符串中有许多的字符。(这些位置不需要对应于一个完整的机器字。)通过使用string-set!过程,一个新的值可以被存进这些位置的一个当中,但是,字符串的位置和以前还是一样的。
从一个位置获取的对象,不管是通过一个变量,还是通过一个类似car,vector-ref或string-ref的过程,在eqv?(11.5小节)的意义来说与在获取之前最后存进的对象是等价的。但,当对象是一个过程时,这是一个例外。当对象是一个过程的时候,从这个位置获取的过程和最后存到这个位置的过程的行为是一样的,但是,它们可能不是相同的对象。(本节已根据勘误表进行修改。)
每一个位置都被标记以指示它是否在被使用。没有变量或对象会指向一个不在使用的位置。不管何时本报告谈及为变量或对象分配位置,这意味着一个适当数量的位置被从未被使用的位置的集合选择,且被选择的位置在变量或对象引用它们之前被标记为现在正在使用。
常数(也就是,字面量表达式的值)被存进只读存储器是可取的。为了表达这个,想象一下,每一个引用位置的对象被用一个标记指示为可变的或不可变的是很方便的。字面常量,symbol->string返回的字符串,不可变区域的记录和其它被显式地被指定为不可变的值是不可变的对象,被本报告列出的其它过程创建的对象是可以改变的。尝试给一个引用不可改变对象的位置赋值应该抛出一个条件类型是&assertion的异常。
5.11. 严格尾递归(Proper tail recursion)
Scheme的实现必须是严格尾递归的。发生在叫做尾上下文(tail contexts)的特定句法上下文的过程调用是尾调用(tail calls)。一个Scheme实现是严格尾递归的,如果它支持无限数量的活动尾调用。一个调用时活动的(active)如果被调用的过程可以返回。注意,这里所说的返回既包括了通过当前继续从调用中返回的情况,也包括了事先以call-with-current-continuation过程捕获继续,后来再调用该继续以便从调用中返回的情况。不存在被捕获的继续时,调用最多只能返回一次,活动调用就是那些还没有返回的调用。一个严格尾递归的正式定义可以在Clinger的论文17中被发现。来自(rnrs base (6))库的结构上识别尾调用的规则在11.20小节被描述。
5.12. 动态生存期(Dynamic extent)和动态环境
对于一个过程调用,在它开始和它返回的过程之间的时间叫做它的动态生存期。在Scheme中,call-with-current-continuation(11.15小节)允许在它的过程调用返回之后重新进入一个动态生存期。因此,一个调用的动态生存期可能不是一个单独的,连贯的时间段。
本报告描述的一些操作除了从它们显式的参数中获得信息,还会从动态环境中获得信息。比如,call-with-current-continuation会访问一个dynamic-wind(11.15小节)创建的隐式的上下文,且抛出过程(库的第7.1小节)访问当前的异常处理程序。改变动态环境的操作是动态的,为了类似dynamic-wind或with-exception-handler的过程的调用的动态作用域。当这样的调用返回时,前一个动态环境被恢复。动态环境可以被认为是一个调用的动态生存期的一部分。因此,它可以被call-with-current-continuation捕获,并可以通过调用它创建的逃逸过程恢复。