Scheme
一个计算机编程语言 来自维基百科,自由的百科全书
Scheme是一种函数式编程语言,是Lisp的两种主要方言之一,不同于与之并列的Common Lisp,Scheme遵循极简主义哲学,以一个小型语言核心作为标准,加上各种强力语言工具(语法糖)来扩展语言本身[19]。Scheme是第一个使用静态作用域的Lisp方言,也是第一个引入头等续体和“干净宏”的编程语言。
![]() | |
编程范型 | 多范型:函数式, 指令式, 元编程 |
---|---|
语言家族 | Lisp |
设计者 | 小盖伊·史提尔和杰拉德·杰伊·萨斯曼 |
发行时间 | 1975年 |
当前版本 |
|
型态系统 | 强类型,动态类型 |
作用域 | 词法 |
文件扩展名 | .scm .ss |
网站 | https://www.scheme.org/ |
主要实作产品 | |
支持R7RS:BiwaScheme[2], Chibi[3], Chicken, Cyclone[4], Foment[5], Gambit, Gauche, Gerbil[6], GNU Guile, Kawa, Larceny[7], LIPS[8], Loko[9], MIT/GNU Scheme, Mosh[10], Picrin[11], Rapid[12], Sagittarius[13], STklos, TR7[14], Ypsilon[15] 其他:Bigloo, Chez, IronScheme, GNU Mes[16], Scheme 48, SCM, TinyScheme | |
衍生副语言 | |
femtolisp[17], Racket, SIOD, T | |
启发语言 | |
ALGOL, Lisp, MDL | |
影响语言 | |
Clojure, Common Lisp, Dylan, EuLisp, Haskell, Hop.js, ISLISP, JavaScript, Julia, Lua, R, Racket, Ruby, Rust, S, Scala, Swift LispKit[18] |
简介
在1975年,麻省理工学院的杰拉德·杰伊·萨斯曼与盖伊·史提尔二世,开发出了Scheme语言最初版本,随后两人通过发表“λ论文集”而不断对它进行完善和推广。Scheme与λ演算关系十分密切,故将小写字母“λ”用作标志。
麻省理工学院与其他一些院校,曾采用Scheme教授计算机科学入门课程。著名的入门教材《计算机程序的构造和解释》(SICP),利用Scheme来诠释程序设计[20]。Scheme有众多实现可视为一个主要优势[21],然而不同实现之间的差异成为了它的一个劣势,Scheme掌控委员会声称,它是“世上最不可移植的编程语言”,并且是一个“编程语言家族”而非一个单一的语言[22]。
历史

Scheme起源于1958年由约翰·麦卡锡提出的Lisp语言。麦卡锡通过Lisp证明了,经由几个简单的算子,与用作匿名函数的借鉴自阿隆佐·邱奇的λ表示法[23],就可以构建出图灵完备的系统。麦卡锡提出的S-表达式,可以将程序与数据用相同的结构存储,这被称为同像性。Scheme的语法即来自S-表达式,这一特性使得在Scheme中实现自循环直译器变得非常简单[24]。
在1973年,麻省理工学院的Carl Hewitt提出的一种叫做演员模型的计算模型[25],并用Lisp开发当时叫做Planner-73的新语言来实现它[26]。杰拉德·萨斯曼与盖伊·史提尔为了理解演员模型,决定在Maclisp工作环境中实现一个微型Lisp解释器,并接着增加创建演员和发送消息的机制[27]。两人很快发现了演员模型与λ演算之间的相似性,所谓“演员”就是彼得·兰丁提出的闭包[28],而Joel Moses在1970年已将它介入Lisp用来解决函数参数问题[29];故而实现演员的关键,是将词法作用域介入到Lisp中[30]。
在1975年,基于对λ演算表达能力的理解[31],杰拉德·萨斯曼与盖伊·史提尔开发出了新型Lisp解释器[32],并将在其上完成的微缩版的演员实现命名为“Schemer”,后因ITS作业系统文件名的字符数限制而改为Scheme[33]。尽管Hewitt指出了Scheme不能表达特定类型的原语演员[34],Scheme解释器本身采用的简约的语法和语义,很快赢得广泛接受。
在1978年,盖伊·史提尔二世和杰拉德·杰伊·萨斯曼发表了《修订的Scheme报告:一种LISP方言》[35],从而完善了Scheme的功能,并正式将其确立为Lisp的一种主要方言。在1998年,二人在总结Scheme历史时指出,简单而强大的λ演算,使得Scheme最终得以实现极度的精简化[36]。
“λ论文集”是杰拉德·萨斯曼与盖伊·史提尔撰写的关于Scheme的一系列论文,最早作为麻省理工学院的内部备忘录发表[37]。通常认定λ论文集包括:
- 1975年: Scheme: An Interpreter for Extended Lambda Calculus.[38]
- 1976年: Lambda: The Ultimate Imperative.[39]
- 1976年: Lambda: The Ultimate Declarative.[40]
- 1977年: Debunking the 'Expensive Procedure Call' Myth, or, Procedure Call Implementations Considered Harmful, or, Lambda: The Ultimate GOTO.[41]
- 1978年: The Art of the Interpreter or, the Modularity Complex (Parts Zero, One, and Two).[42]
- 1978年: RABBIT: A Compiler for SCHEME.[43]
- 1979年: Design of LISP-based Processors, or SCHEME: A Dialect of LISP, or Finite Memories Considered Harmful, or LAMBDA: The Ultimate Opcode.[44]
- 1980年: Compiler Optimization Based on Viewing LAMBDA as RENAME + GOTO. AI: An MIT Perspective.[45]
- 1980年: Design of a Lisp-based Processor. CACM. 23. 11.[46]
Scheme的业界标准,是由专门的掌控委员会发表的《第n次修订的演算法语言Scheme报告》(Revisedn Report on the Algorithmic Language Scheme),这种标题形式参照了ALGOL 60标准文档的标题[47]。Scheme曾经由IEEE标准化为IEEE 1178–1990,它于2019年11月07日停用[48]。
1998年通过的R5RS是现在被普遍接受的标准[49]。2007年通过的R6RS[50],做出了很大的变动[51],Scheme社区中有使用者指责它在堆积华而不实的功能[52][53]。Scheme掌控委员会决定在R7RS中,将Scheme分化为两个独立而兼容的语言[54]:一个是2013年通过的R7RS-small[55],它为教学场合提供对IEEE/R5RS标准的及时更新,R7RS-small目前已经有了一些实现支持[56];而另一个是作为标准库合集的R7RS-Large[57],它包含了各种提议被接纳并进入冻结状态的Scheme实现要求(SRFI),以此为实际编程场合提供对R6RS标准的继续完善,现已发表了有关数据结构部份的R7RS-Large过渡草案红色版[58]。
命名法
在正式场合比如Scheme标准的命名法中,提及一个λ表达式或原始过程时,偏好使用词语“过程”而非“函数”。在一般使用中,词语“过程”和“函数”是可互换使用的。过程应用有时被正式称呼为“组合”(combination)。
同其他Lisp一样,在Scheme中使用术语“thunk”,来提及没有实际参数的过程。术语“适当”(proper)尾递归,称谓所有Scheme实现的一个性质,它们都进行尾递归优化,从而支持无限数目的活跃尾递归。
基础特征
Scheme大体上是一个函数式程式语言,并支援其他编程范型。它同其他Lisp编程语言家族语言共享了很多特征。它的非常简单的语法基于Lisp的S-表达式:即圆括号包围的列表,其中的前缀是算符,而随后那些的是实际参数。故而Scheme程序由嵌套的列表的序列构成。列表也是Scheme中的主要数据结构,这导致了在源代码和数据格式之间的紧密等价性,即同像性。Scheme程序可以轻易的动态建立和求值Scheme代码片段。
Scheme与其他Lisp方言的列表,都是基于最基础的数据结构有序对(pair)。Scheme从它的Lisp先辈继承了一组丰富的列表处理原始运算,比如cons
、car
和cdr
。Scheme的变数都使用动态强型别系统,Scheme中的过程都是头等物件,即头等函数,因此过程可以作为值赋值给变量,或作为实际参数传递给过程。
本章主要集中于语言的创新特征,它们使得Scheme区别于其他Lisp方言。除非专门指出,这里描述的特征都有关于R5RS标准。在本章例子中使用“===> 结果
”表示法,来指示求值在紧前行上的表达式的结果。这同于R5RS中使用的约定。本章节描述的特征使得Scheme不同于在它之前的其他编程语言。Scheme的这些方面强烈的影响了Scheme语言的所有产品,并共通于始自1975年最初的λ论文,特别是自从R2RS以来[59],所有版本的Scheme编程语言。
Scheme的简约性,使它成为具备同级别功能的程式语言中最易于实现的语言,这得益于使用λ演算来从更原始的形式导出多数的语言语法[60]。例如在R5RS标准中,定义了23个基于S-表达式的语法构造,其中14个被归类为派生形式或库形式,它们可以被写为涉及原则上为lambdad的更基础形式的宏。正如R5RS(§3.1)所说:“最基础的变量绑定构造是lambda表达式,因为所有其他的变量绑定构造,都可以依据lambda表达式来做出解释”[49]。
- 基础形式:
define
、lambda
、quote
、if
、define-syntax
、let-syntax
、letrec-syntax
、syntax-rules
、set!
- 派生形式:
do
、let
、let*
、letrec
、cond
、case
、and
、or
、begin
、命名let
、delay
、unquote
、unquote-splicing
、quasiquote
下面是一个宏的例子,它将let
实现为使用lambda
来进行变量绑定的一个表达式:
(define-syntax let
(syntax-rules ()
((let ((var expr) ...) body ...)
((lambda (var ...) body ...) expr ...))))
这里的(let ((var expr) ...) body ...)
称为“模式”,模式中起始的let
被称为“关键字”,syntax-rules ()
的空括号中可添入作为辅助关键字的叫做“文字”的标识符,var
、expr
和body
称为“模式变量”,...
是称为“省略号”的标识符,它表示所跟随的子模式可以匹配零或多个输入中的元素;而((lambda (var ...) body ...) expr ...)
称为“模板”。使用上述定义的let
,一个Scheme实现可以将(let ((a 1)(b 2)) (+ b a))
,重写为((lambda (a b) (+ b a)) 1 2)
,这将实现任务缩减为编码过程实例化的工作。
不像更早期的Lisp比如Maclisp[61],Scheme像多数现代语言一样,采用了词法作用域[62]:在一个程序单元中所有可能的变量绑定,都可以通过阅读这个程序单元来分析出来,而不需要考虑到它可能在其中被调用的那些上下文。这对比于动态作用域,它是早期Lisp方言的特征,因为在当时的编译器和解释器中,用来实现词法作用域算法的原始的文字替换方法,关联着处理代价。在动态作用域的Lisp中,对一个过程内的自由变量的引用,依赖于这个调用的上下文,完全有可能引用到这个过程外部的相当不同的绑定。
邱奇的λ演算的数学表示法,启发了Lisp使用lambda
作为介入一个过程的关键字,并影响了Lisp中涉及到使用高阶函数的函数式编程技术的发展。但是早期的Lisp由于对自由变量的处理方式[63],而不适合表达λ演算[31]。
一个正式的λ系统,拥有一些公理和一个完备的推理规则。这有助于使用数理逻辑和工具来进行分析。在这个系统中,演算可以被看作直接的演绎。λ演算的语法,来自用x, y, z, ...
、圆括号、空格、点号和符号λ形成的递归表达式[64]。λ演算的功能包括:首先,充当强力的数理逻辑的起点。其次,它可以缩减编程者在考虑实现细节上的要求,因为它可以用于模拟机器求值。最后,λ演算建立了一个坚实的元理论[65]。
在Scheme中,lambda
关键字被用于定义匿名过程,并且使用define
基础形式来定义命名过程,即将一个lambda
过程绑定到指名的全局变量[66]。在Scheme中,(define (foo x) (+ x 1))
与(define foo (lambda (x) (+ x 1)))
,在语法上是等同的。例如有序对可以表示为[67]:
(define (cons x y)
(lambda (m) (m x y)))
(define (car z)
(z (lambda (p q) p)))
(define (cdr z)
(z (lambda (p q) q)))
这样定义出来的cons
、car
和cdr
,满足恒等式(car (cons x y))
等于x
,和(cdr (cons x y))
等于y
。甚至递归也可以表示为利用λ演算的Y组合子[68]。
词法作用域的介入[62],通过在某些形式的λ表示法,和在工作编程语言中它们的实际表达之间做出等价,解决了早期Lisp的有关问题。Sussman和Steele展示了,通过将λ表达式用作“控制结构和环境修改器”,而非简单的过程实例化,可以用这个新语言优雅的导出其他编程语言包括ALGOL和Fortran的,所有指令式和声明式语义,和其他Lisp的动态作用域[69]。他们在第一篇λ论文中,与对Scheme的首次描述一起,介入了续体传递风格[70],并在后续的论文中,他们推进演示了在这种实际使用中体现出的λ演算的原生能力。
Scheme采用了传值调用,但不同于多数Lisp规定了过程实际参数的求值次序,Scheme不规定求值次序。对比于其他Lisp,Scheme表达式在算符位置(第一个项目)上可以是一个表达式,只要在算符位置上的表达式的结果是一个过程,这种表示就是完全合法的[71]。
在Scheme中,在算符和实际参数位置上的这些表达式的求值次序,可以在逐个调用的基础上由实现来选择,而唯一的约束是:“运算符和运算数表达式的任何并发求值的效果,被约束为一致于某种顺序次序的求值。”(R5RS sec. 4.1.3)[49]
(let
((ev (lambda (n)
(display "Evaluating ")
(display (if (procedure? n) "procedure" n))
(newline)
n)))
((ev +) (ev 1) (ev 2)))
ev
是一个过程,它描述传递给它的实际参数,接着返回这个实际参数的值。在调用过程+
来加1
和2
之中,表达式(ev +)
、(ev 1)
和(ev 2)
可以按任何次序求值,只要效果上它们不像是并行求值的。因此在上述例子被执行的时候,标准Scheme可以按任何次序来显示前三行:
Evaluating procedure
Evaluating 1
Evaluating 2
3
然而一行的文本不可以同另一行的文本产生交错,因为这会违反顺序求值约束。
Scheme的代码块结构,不同于早先Lisp的语言构造[72]。自从R2RS开始[59],Scheme中的块,是通过三个“绑定构造”来实现的:let
、let*
和letrec
。例如,下列构造建立一个块,其中叫做var
的符号被绑定到数10
:
(define var "goose")
;; 这里任何到var的引用都会被绑定到"goose"
(let ((var 10))
;; 语句走到这里时,任何到var的引用都会绑定到10
)
;; 这里任何到var的引用都会绑定到"goose"
依据编程者的需要,块可以嵌套来建立任意复杂的块结构。使用块结构来建立局部绑定,减轻了不然可能发生的命名空间冲突。
let
的变体之一let*
,允许绑定引用在前面相同构造中定义的变量,例如:
(let*
((var1 10)
(var2 (+ var1 12)))
;; 但是var1的定义不可以引用var2
)
let
的另一个变体letrec
,被设计用来确使互递归过程可绑定彼此。
下面的例子是侯世达雌雄序列:
;; 将侯世达雌雄序列计算为有序对的列表
(define (hofstadter-male-female n)
(letrec
((female (lambda (n)
(if (= n 0)
1
(- n (male (female (- n 1)))))))
(male (lambda (n)
(if (= n 0)
0
(- n (female (male (- n 1))))))))
(let loop ((i 0))
(if (> i n)
'()
(cons (cons (female i) (male i))
(loop (+ i 1)))))))
(hofstadter-male-female 8)
===> ((1 . 0) (1 . 0) (2 . 1) (2 . 2) (3 . 2) (3 . 3) (4 . 4) (5 . 4) (5 . 5))
在一个单一的letrec
中的所有过程,可以通过名字引用其他的过程,还有在相同的letrec
中此前定义的变量的值,但是它们不可以引用在相同的letrec
中此后定义的变量的值。
let
的变体“命名let
”形式,在let
关键字之后有一个标识符。它将这些let
变量绑定到一个过程的实际参数,这个过程的名字由这个标识符给出,它的过程体是let
形式的主体。这个过程体可以通过调用这个过程达成想要的重复。命名let
被广泛用于实现迭代。
一个简单的计数器例子:
(let loop ((n 1))
(if (> n 10)
'()
(cons n
(loop (+ n 1)))))
===> (1 2 3 4 5 6 7 8 9 10)
就像Scheme中的任何过程一样,在命名let
中建立的这个过程是头等对象。
Scheme拥有一个迭代构造do
[73],但是在Scheme中更推崇的惯用法,是使用尾递归来表达迭代[74]。遵循标准的Scheme实现被要求优化尾递归,从而支持无限数量的活跃尾递归(R5RS sec. 3.5[49]),这个性质被Scheme报告描述为“适当”(proper)尾递归,它使得Scheme编程者可以安全的使用递归结构来书写迭代算法,这在很多时候是更符合直觉的。尾递归过程和命名let
形式,提供对使用尾递归的迭代的支持。
;; 建造从0到9的平方的列表:
;; 注意:loop简单的就是用作标签的一个任意符号。任何符号都行。
(define (list-of-squares n)
(let loop ((i n) (res '()))
(if (< i 0)
res
(loop (- i 1) (cons (* i i) res)))))
(list-of-squares 9)
===> (0 1 4 9 16 25 36 49 64 81)
在Scheme中续体是头等对象[75]。自从R2RS开始[59],Scheme提供了过程call-with-current-continuation
,简写为call/cc
,它捕获当前续体,将其包装成逃脱(escape)过程,再绑定到编程者提供的一个过程的唯一形式参数上。如果逃脱过程在后来某个时候被调用,它会放弃在后来时候正生效的任何续体,转而使用在创建这个逃脱过程之时生效的那个续体。逃脱过程与原本调用call/cc
的续体,接受相同数目的实际参数;除了用call-with-values
创建的续体,所有续体恰好接受一个值(R5RS sec. 6.4)[49]。
头等续体使得编程者能够建立非局部控制结构比如迭代器、协程和回溯。续体可以被用来模拟指令式编程语言中return语句的行为。下列函数find-first
接受函数func
和列表lst
,返回lst
中的第一个元素x
,它使得(func x)
返回真。
(define (find-first func lst)
(call/cc
(lambda (return)
(for-each
(lambda (x)
(if (func x)
(return x)))
lst)
#f)))
(find-first integer? '(1/2 3/4 5.6 7 8/9 10 11))
===> 7
(find-first zero? '(1 2 3 4))
===> #f
David Madore在1999年提出的阴阳谜题[76],展示了Scheme可以将续体当作头等对象处理,绑定它们到变量,和把它们作为给过程的实际参数来传递[77]。
对比于Common Lisp,在Scheme中所有的数据和过程共享一个共同的命名空间,而Common Lisp中有函数和数据分离的命名空间,使得一个函数和一个变量可以有相同的名字,并且将一个函数作为值引用时要求特殊的表示法。这有时叫做“Lisp1与Lisp2”差异,二者分别称谓Scheme的统一的命名空间,和Common Lisp的分立的命名空间[78]。
在Scheme中, 可以使用操纵和绑定数据的原始运算来绑定过程。没有等价于Common Lisp的defun
和#'
的原始运算。
;; 变量绑定到一个数:
(define f 10)
f
===> 10
;; 变化(改变绑定值)
(set! f (+ f f 6))
f
===> 26
;; 将一个过程赋值给相同的变量:
(set! f (lambda (n) (+ n 12)))
(f 6)
===> 18
;; 将一个表达式的结果赋值给相同的变量:
(set! f (f 1))
f
===> 13
;; 函数式编程:
(apply + '(1 2 3 4 5 6))
===> 21
(set! f (lambda (n) (+ n 100)))
(map f '(1 2 3))
===> (101 102 103)
实现标准
本章归档多年来做出的给与Scheme特定特征的设计决定,它们不是最初设计的直接产物。
直到R5RS标准,在Scheme中的标准注释是分号,它使得这行余下部份对Scheme不可见。许多实现支持可替代的约定,允许注释扩展为多于一行,而R6RS标准允许其中的两种:一个完整的S-表达式,可以通过前导#;
而变成一个注释(介入于SRFI 62[79]),和通过用#|
和|#
围绕文本,产生的“多行注释”或“块注释”。
在多数Lisp方言包括Common Lisp中,布尔表达式中的值NIL
按照惯例被求值为值假。在Scheme中,自从1991年的IEEE标准[48],除了#f
的所有的值,包括在Scheme中写为'()
的NIL
的等价者,在布尔表达式中求值为值真(R5RS sec. 6.3.1)[49]。
在多数Lisp中表示布尔值真的常量是T
,而在Scheme中它是#t
。
Scheme的语法可以轻易的通过宏系统来扩展[80]。R5RS标准介入了强力的干净宏系统[81],它允许编程者使用一种简单的模式匹配子语言,向语言增加的新的语法构造(R5RS sec 4.3)[49]。在此前的R4RS标准中,干净宏系统已经作为“高级”系统,和“低级”宏系统一起被编排入标准的附录中[82],二者都被当作对Scheme的扩展而非语言的本质部份。
干净宏的实现,也叫做syntax-rules
, 被要求遵守语言的其余部份的词法作用域。这是通过针对宏展开的特殊命名和作用域规则来确保的,从而避免在其他编程语言的宏系统中可能出现的常见编程错误。R6RS规定了更加复杂的变换系统syntax-case
,它已经作为对R5RS Scheme的一个语言扩展而能够获得到有一段时间了。例如:
;; 定义一个宏来实现“if”的具有多个表达式的变体
;; 有真分支而无假分支
(define-syntax when
(syntax-rules ()
((when pred exp exps ...)
(if pred (begin exp exps ...)))))
宏和过程的调用看起来非常相似,二者都是S-表达式,但是它们被不同的对待。在编译器遇到程序中的一个S-表达式的时候,它首先查看这个符号是否被定义为在当前词法范围内的语法关键字。如果是这样,它接着尝试展开这个宏,将在这个S-表达式尾部的项目当作实际参数,而不用编译代码来求值它们,递归的重复这个处理过程直到没有余留的宏调用。如果它不是一个语法关键字,编译器编译代码来求值在这个S-表达式尾部的实际参数,并接着求值在这个S-表达式头部的符号所表示的变量,把它作为过程来调用,并把最终的尾部表达式作为实际参数传递给它。
多数Scheme实现还提供额外的宏系统。其中最流行是语法闭包、显式重命名宏和define-macro
,它是类似于Common Lisp中提供的defmacro
系统的非干净宏。
不能指定一个宏是否为干净的,是宏系统的一个缺点。可作为替代的展开模型比如作用域集合,提供一种潜在的解决方案[83]。
自从R2RS开始[59],Scheme通过delay
形式和过程force
支持延迟求值。
(define a 10)
(define eval-aplus2 (delay (+ a 2)))
(set! a 20)
(force eval-aplus2)
===> 22
(define eval-aplus50 (delay (+ a 50)))
(let ((a 8))
(force eval-aplus50))
===> 70
(set! a 100)
(force eval-aplus2)
===> 22
delay
原始运算,产生叫做promise的一个对象,它在将来的某个时间点上被询问从而递交结果值,promise的原本定义的文字内容会被留存,并且在第一次使用force
之后它的值也会被留存。promise永远只求值一次。它们可以被用来实现高级的惰性求值构造比如串流[84]。
在R5RS中,给出了delay
和force
的推荐实现,将promise实现为没有实际参数的一个过程(thunk),并使用记忆化来确保它永远只求值一次,与调用force
的次数无关(R5RS sec. 6.4)[49]。在R6RS标准中,它们不再是原始运算,转而作为R5RS兼容库的一部份提供(rnrs r5rs (6))。
SRFI 41确使表达有限和无限序列能够具有非凡的经济性。例如,这是使用在SRFI 41中的函数定义的一个斐波那契数列[84]:
;; 定义斐波那契序列
(define fibs
(stream-cons 0
(stream-cons 1
(stream-map +
fibs
(stream-cdr fibs)))))
;; 计算这个序列的第一百个数
(stream-ref fibs 99)
===> 218922995834555169026
R5RS标准之前,在其他Lisp中无处不在的eval
过程,在Scheme中没有等价者。在第一篇λ论文中,曾经将evaluate
描述为“类似于LISP函数EVAL”[85],但在具有词法作用域的Scheme中,对这个表达式在哪个环境中求值存在困惑。例如,不明确求值下列表达式的结果应当是5
还是6
[86]:
(let ((name '+))
(let ((+ *))
(evaluate (list name 2 3))))
在求值实际参数name
的时候,在外层环境中找到了它的定义;在求值结果表达式(+ 2 3)
的时候,如果在外层环境中求值,结果是两个运算数的总和;如果在内层环境中求值,这里符号+
已经被绑定到过程*
的值,结果是两个运算数的乘积。为此在1978年的最初修订报告中,将evaluate
替代为enclose
,它接受分别为代码和运行环境的两个实际参数[87]。由于各种技术和实际原因,第二次、第三次和第四次修订报告省略了任何eval
的等价者[86]。
R5RS通过规定三个返回环境的过程,并提供接受一个S-表达式和一个环境的过程eval
,它在提供的这个环境内求值这个表达式(R5RS sec. 6.5)[49],解决了这个困惑。R6RS通过提供叫做environment
的一个过程进行了扩展,编程者通过它可以精确的指定将哪个对象导入到求值环境之内。
通过现代Scheme(通常兼容于R5RS)来求值表达式expr
,你需要定义如下这样的一个函数evaluate
:
(define (evaluate expr)
(eval expr (interaction-environment)))
interaction-environment
是解释器的全局环境。
Scheme规定了数值数据类型的相较完全的集合,包括复数和有理数类型,这在Scheme中叫做“数值塔”(R5RS sec. 6.2[49])。标准对它们作抽象处理,而不委以任何特定的内部表示。
数可以有精确性品质。一个精确数只能从涉及其他精确数的精确运算产生,不精确是传染的。标准规定任何两个实现对产生精确数的所有运算都必须产生等价的结果。
R5RS标准规定了过程exact->inexact
和inexact->exact
,它们可以用于改变一个数的精确性。inexact->exact
产生“在数值上最接近实际参数的精确数”。exact->inexact
产生“在数值上最接近实际参数的不精确数”。R6RS标准在主报告中省略了这些过程,而是在标准库中将它们规定为R5RS兼容过程(rnrs r5rs (6))。
在R5RS标准中,不要求Scheme实现去实现整个数值塔,而是它们必须实现“符合实现用途和Scheme语言精神二者的连贯子集”(R5RS sec. 6.2.3)[49]。新的R6RS标准要求实现整个数值塔,并且“精确整数对象和精确有理数对象实际上有无限的大小和精度,并且对于实现特定过程...所以它们在得到精确实际参数时总是返回精确结果”(R6RS sec. 3.4, sec. 11.7.1)[50]。
在支持精确有理复数的实现中的精确算术例子:
;; 三个有理实数和两个有理复数的总和
(define x (+ 1/3 1/4 -1/5 -1/3i 405/50+2/3i))
x
===> 509/60+1/3i
;; 检查精确性
(exact? x)
===> #t
在既不支持精确有理数也不支持复数却接受有理数表示法的实数的实现中相同算术的例子:
;; 四个有理实数的总和
(define xr (+ 1/3 1/4 -1/5 405/50))
;; 两个有理实数的总和
(define xi (+ -1/3 2/3))
xr
===> 8.48333333333333
xi
===> 0.333333333333333
;; 检查精确性
(exact? xr)
===> #f
(exact? xi)
===> #f
二者实现都符合R5RS标准,但是第二个实现不符合R6RS,因为它没有实现完整的数值塔。
在Scheme中原始数据类型是不相交的。对于任何Scheme对象,下列谓词中只有一个可以为真:boolean?
、pair?
、symbol?
、number?
、char?
、>string?
、vector?
、port?
和procedure?
(R5RS sec 3.2)[49]。
作为对比,在数值数据类型之内,对数值值是可以有交叠的。例如,一个整数值可以同时满足如下谓词:integer?
、rational?
、real?
、complex?
和number?
(R5RS sec 6.2)[49]。
Scheme在任意对象之间有三种不同类型的等价,它们通过三种不同的“等价谓词”来指示,即测试等式的关系算符eq?
、eqv?
和equal?
:
eq?
求值为#f
,除非它的实际参数表示在内存中相同的对象;eqv?
大体上同于eq?
,但是特殊处理原始对象(比如字符和数),使得表示相同值的数eqv?
为真,即使它们不引用相同的对象;equal?
比较数据结构比如列表、向量和字符串,来确定它们是否有全等的结构并且其内容eqv?
为真(R5RS sec. 6.1)[49]。
在Scheme中还存在依赖于类型的等价运算:string=?
和string-ci=?
比较两个字符串(后者进行不依赖大小写比较);char=?
和char-ci=?
比较字符;=
比较数[49]。
Scheme的输入和输出基于了“端口”数据类型(R5RS sec 6.6)[49]。R5RS定义了两个缺省端口,可通过过程current-input-port
和current-output-port
来访问,它们对应于Unix概念的标准输入和标准输出。多数实现还提供current-error-port
。标准通过标准过程比如with-input-from-file
和with-output-to-file
,支持输入和标准输出的重定向。多数实现提供有重定向能力的字符串端口,使用在SRFI 6中描述的过程[88],确使很多常规的输入-输出运算能在字符串缓冲区上进行而非在文件上。R6RS标准规定了更多复杂和有能力的端口过程和很多新的端口类型。
下面的例子是使用严格的R5RS Scheme书写的。
例子1,缺省输出到current-output-port
:
(let ((hello0 (lambda() (display "Hello world") (newline))))
(hello0))
例子2,类似例子1但对输出过程使用可选的端口参数的例子:
(let ((hello1 (lambda (p) (display "Hello world" p) (newline p))))
(hello1 (current-output-port)))
类似例子1,但是输出被重定向到一个新建文件:
;; NB: with-output-to-file is an optional procedure in R5RS
(let ((hello0 (lambda () (display "Hello world") (newline))))
(with-output-to-file "helloworldoutputfile" hello0))
类似例子2,但是具有显式的文件打开和端口关闭来发送输出到文件:
(let ((hello1 (lambda (p) (display "Hello world" p) (newline p)))
(output-port (open-output-file "helloworldoutputfile")))
(hello1 output-port)
(close-output-port output-port))
类似例子2,但是使用call-with-output-file
来发送输出到一个文件:
(let ((hello1 (lambda (p) (display "Hello world" p) (newline p))))
(call-with-output-file "helloworldoutputfile" hello1))
对输入提供了类似的过程。R5RS Scheme提供了谓词input-port?
和output-port?
。对于字符输入和输出提供了write-char
、read-char
、peek-char
和char-ready?
。为了书写和阅读Scheme表达式,Scheme提供了read
和write
。在读运算时,如果输入端口到达了文件的结束处,则返回的结果是end-of-file
对象,并且这可以使用谓词eof-object?
来测试。
除了标准之外,SRFI 28定义了一个基本的格式化过程,类似Common Lisp的format
并以此命名[89]。
在Scheme中,过程被绑定到变量[66]。在R5RS中语言标准正式的授权编程者可以变更内建过程的变量绑定,在效果上重定义它们(R5RS "Language changes")[49]。例如,通过重定义+
可以将它扩展为同接受数一样接受字符串:
(set! +
(let ((original+ +))
(lambda args
(apply
(if (or (null? args) (string? (car args)))
string-append
original+)
args))))
(+ 1 2 3)
===> 6
(+ "1" "2" "3")
===> "123"
在R6RS中所有绑定,包括标准过程,都属于某个库,并且所有导出的绑定都是不可变的(R6RS sec 7.1)[50]。因此,禁止通过变更进行标准过程的重定义。转而,可以在标准过程的名字下导入不同的过程,这在效果上类似于重定义。
标准形式和过程概述
Scheme语言正式定义于1998年的R5RS和2007年R6RS标准之中。它们描述了标准“形式”,即关键字和随同的语法,它们提供语言的控制结构,和执行通常任务的标准过程。
在标准Scheme中,从一个数据类型转换到另一个数据类型的过程,在它们的名字中包含字符串->
,谓词结束于一个?
,而改变已经分配了数据的值的过程结束于一个!
。Scheme编程者通常跟从这些命名约定。
本表格描述Scheme中的标准形式。某些形式出现在不止一行之中,因为它们不能被轻易的归类入语言中的一个单一功能。
在表格中标记了“L”的形式被归类为标准中的派生的“库”形式,并且在实践中通常被实现为使用了更基础形式的宏,这使得实现任务比在其他语言中要容易许多。
用途 | 形式 |
---|---|
定义 | define |
绑定构造 | lambda, do (L), let (L), let* (L), letrec (L) |
条件求值 | if, cond (L), case (L), and (L), or (L) |
顺序求值 | begin (*) |
迭代 | lambda, do (L), 命名let (L) |
语法扩展 | define-syntax, let-syntax, letrec-syntax, syntax-rules (R5RS), syntax-case (R6RS) |
引述 | quote('), unquote(,), quasiquote(`), unquote-splicing(,@) |
赋值 | set! |
延迟求值 | delay (L) |
注意begin
在R5RS中被定义为库语法,但是展开者需要知道它来完成分片功能。在R6RS中它不再是库语法。
下面两个表格描述在R5RS Scheme中的标准过程。R6RS更加具有可扩展性而如此总结是不实际的。
某些过程出现在不止一行之中,因为它们不能轻易的分类入语言的一个单一功能。
用途 | 过程 |
---|---|
构造 | vector, make-vector, make-string, list |
等价谓词 | eq?, eqv?, equal?, string=?, string-ci=?, char=?, char-ci=? |
类型转换 | vector->list, list->vector, number->string, string->number, symbol->string, string->symbol, char->integer, integer->char, string->list, list->string |
数值 | 参见独立表格 |
字符串 | string?, make-string, string, string-length, string-ref, string-set!, string=?, string-ci=?, string<? string-ci<?, string<=? string-ci<=?, string>? string-ci>?, string>=? string-ci>=?, substring, string-append, string->list, list->string, string-copy, string-fill! |
字符 | char?, char=?, char-ci=?, char<? char-ci<?, char<=? char-ci<=?, char>? char-ci>?, char>=? char-ci>=?, char-alphabetic?, char-numeric?, char-whitespace?, char-upper-case?, char-lower-case?, char->integer, integer->char, char-upcase, char-downcase |
向量 | make-vector, vector, vector?, vector-length, vector-ref, vector-set!, vector->list, list->vector, vector-fill! |
符号 | symbol->string, string->symbol, symbol? |
有序对和列表 | pair?, cons, car, cdr, set-car!, set-cdr!, null?, list?, list, length, append, reverse, list-tail, list-ref, memq. memv. member, assq, assv, assoc, list->vector, vector->list, list->string, string->list |
识别谓词 | boolean?, pair?, symbol?, number?, char?, string?, vector?, port?, procedure? |
续体 | call-with-current-continuation (call/cc), values, call-with-values, dynamic-wind |
环境 | eval, scheme-report-environment, null-environment, interaction-environment (可选) |
输入/输出 | display, newline, read, write, read-char, write-char, peek-char, char-ready?, eof-object? open-input-file, open-output-file, close-input-port, close-output-port, input-port?, output-port?, current-input-port, current-output-port, call-with-input-file, call-with-output-file, with-input-from-file(可选), with-output-to-file(可选) |
系统接口 | load (可选), transcript-on (可选), transcript-off (可选) |
延迟求值 | force |
函数式编程 | procedure?, apply, map, for-each |
布尔逻辑 | boolean? not |
在其名字中包含-ci
的字符串和字符过程,在它们的实际参数之间进行不依赖大小写的比较:相同字符的大写和小写版本被认为是相等的。
用途 | 过程 |
---|---|
基本算术算符 | +, -, *, /, abs, quotient, remainder, modulo, gcd, lcm, expt, sqrt |
有理数 | numerator, denominator, rational?, rationalize |
近似 | floor, ceiling, truncate, round |
精确性 | inexact->exact, exact->inexact, exact?, inexact? |
不等式 | <, <= , >, >=, = |
杂项谓词 | zero?, negative?, positive? odd? even? |
极大和极小 | max, min |
三角 | sin, cos, tan, asin, acos, atan |
指数 | exp, log |
复数 | make-rectangular, make-polar, real-part, imag-part, magnitude, angle, complex? |
输入-输出 | number->string, string->number |
类型谓词 | integer?, rational?, real?, complex?, number? |
接受多于二个实际参数的-
和/
在R5RS中被定义了但留作为可选的。
Scheme实现要求
由于Scheme的极简主义,很多常见过程和语法形式不是由标准定义的。 为了保持核心语言很小并促进扩展的标准化,Scheme社群拥有“Scheme实现要求”(SRFI)流程,通过对扩展提案的仔细研讨来定义扩展库。这提升了代码可移植性。很多SRFI被几乎所有Scheme实现支持。
在不同的实现中具有相当广泛支持的SRFI包括[90]:
- 0: 基于特征的条件展开构造
- 1: 列表库
- 4: 同质数值向量数据类型
- 6: 基本字符串端口
- 8: 接收、绑定到多个值
- 9: 定义记录类型
- 13: 字符串库
- 14: 字符集库
- 16: 可变元数过程的语法
- 17: 广义
set!
- 18: 多线程支持
- 19: 时间数据类型和过程
- 25: 多维数组原语
- 26: 不用柯里化的特殊化形式参数的表示法
- 27: 随机数位的来源
- 28: 基本格式化字符串
- 29: 本地化
- 30: 嵌套的多行注释
- 31: 递归求值的特殊形式
- 37:
args-fold
:程序实际参数处理器 - 39: 形式参数对象
- 41: 串流
- 42: 及早推导式
- 43: 向量库
- 45: 表达迭代式惰性算法的原语
- 60: 作为位元的整数
- 61: 更一般性的
cond
子句 - 66: 八位组向量
- 67: 比较过程
实作
Scheme的精简设计,使得程式语言设计人士与爱好者,特别钟爱研究它,故而它有不断涌现的新实作,而活跃开发的实作也在持续跟进语言标准更新[91]。尽管Scheme有众多实现是它的一个主要长处[21],但是由于Scheme没有库函数标准,造成了很多不可互通的实作互相竞争,试图使用Scheme编写既复杂又便于移植的程序,往往比较困难[22]。R6RS和R7RS-Large,在核心语言之外制定了库函数标准,使得编译器开发者和贡献者可以实现Scheme的可移植库。
几乎所有Scheme实作都有传承自Lisp的“读取﹣求值﹣输出循环”交互模式,一些Scheme实作亦可作为编译器。很多用C语言及衍生语言写成的软体,都利用Scheme作为脚本语言,很多嵌入式系统语言即是基于Scheme。Chez Scheme和Larceny等实现,可以将Scheme程式编译为本机二进制码。Gambit和Chicken等实现,可将Scheme程式编译为C代码,Bigloo还能编译成JVM字节码或者.Net字节码。
一些实现支持额外的特征。比如Kawa提供与Java类的集成,而Scheme到C编译器通常易于使用C写成的外部库,甚至允许将实际C代码嵌入到Scheme源代码中。另一个例子是用java书写的Pvts,它提供了支持Scheme学习的一组可视工具。
教科书
实际用处
很多著名的电脑科学院校都利用Scheme来教授入门级课程。以下为一些最为著名的教授Scheme的学校:
- 麻省理工学院是Scheme与SICP的诞生地。直到2008年为止,麻省理工学院的入门课程6.001即是用Scheme来教授的[93]。尽管现在Scheme已经不再被用于入门课程,麻省理工学院到目前为止还在教授SICP[94]。
- 柏克莱加州大学的入门课程61A到2010年为止利用Scheme与SICP教授入门课程,并利用Scheme来实作Logo,另一个基于Lisp的程式语言[95]。自2011年起,61A改用Python来教授SICP[96]。
- 西北大学的入门课程CS2500利用Scheme来教授另一本著名的教材《程序设计方法》。
- 印第安那大学的入门课程C211利用Scheme来教授。
- 耶鲁大学
- 莱斯大学
- 香港科技大学
- 北京大学
- ProgramByDesign项目在美国超过600所高中教授Scheme语言。
- 滑铁卢大学数学系(包括计算机科学系)的入门课程CS115、CS116、CS135利用Scheme来教授。
- 云林科技大学
自由软体影像处理程式GIMP利用Scheme为嵌入式脚本语言[97]。GNOME中有到核心库的一个GNU的扩展语言Guile包装器[98]。在2012年出现的Julia所采用的语法解析器,是用Scheme方言Femtolisp实现的。
参见
注释
延伸阅读
外部链接
Wikiwand - on
Seamless Wikipedia browsing. On steroids.