Scheme是一种函数式编程语言,是Lisp的两种主要方言之一,不同于与之并列的Common Lisp,Scheme遵循极简主义英语Minimalism (computing)哲学,以一个小型语言核心作为标准,加上各种强力语言工具(语法糖)来扩展语言本身[19]。Scheme是第一个使用静态作用域的Lisp方言,也是第一个引入头等续体和“干净宏”的编程语言。

Quick Facts 编程范型, 语言家族 ...
Scheme
Thumb
编程范型多范型函数式, 指令式, 元编程
语言家族Lisp
设计者小盖伊·史提尔杰拉德·杰伊·萨斯曼
发行时间1975年,​49年前​(1975
当前版本
  • R7RS-small(2013;稳定版本)[1]
编辑维基数据链接
类型系统强类型动态类型
作用域词法
文件扩展名.scm   .ss
网站www.scheme-reports.org 编辑维基数据链接
主要实现产品
Bigloo英语Bigloo, BONES[2], Chez, Chibi[3], Chicken, Cyclone[4], Foment[5], Gambit, Gauche英语Gauche (Scheme implementation), Guile, IronScheme英语IronScheme, Kawa英语Kawa (Scheme implementation), Larceny, LIPS[6], Loko[7], MIT/GNU Scheme, Mosh[8], Picrin[9], Rapid[10], s7[11], S9fES[12], Sagittarius[13], Scheme 48, SCM, STklos英语STklos, TinyScheme, TR7[14]
派生副语言
femtolisp[15], Husk[16], Racket, SIOD, Swift LispKit[17], T英语T (programming language)
启发语言
ALGOL, Lisp, MDL英语MDL (programming language)
影响语言
Clojure, Common Lisp, Dylan, EuLisp英语EuLisp, Haskell, Hop英语Hop (software), ISLISP, JavaScript, Julia, Lua, R, Racket, Ruby, Rust, S, Scala
Close

简介

在1975年,麻省理工学院杰拉德·杰伊·萨斯曼盖伊·史提尔二世,开发出了Scheme语言最初版本,随后两人通过发表“λ论文集”而不断对它进行完善和推广。Scheme与λ演算关系十分密切,故将小写字母“λ”用作标志

麻省理工学院与其他一些院校,曾采用Scheme教授电脑科学入门课程。著名的入门教材《计算机程序的构造和解释》(SICP),利用Scheme来诠释程式设计[20]。Scheme有众多实现可视为一个主要优势[21],然而不同实现之间的差异成为了它的一个劣势,Scheme掌控委员会声称,它是“世上最不可移植的编程语言”,并且是一个“编程语言家族”而非一个单一的语言[22]

历史

起源

Thumb
Gerald Jay Sussman

Scheme起源于1958年由约翰·麦卡锡提出的Lisp语言。麦卡锡通过Lisp证明了,经由几个简单的算子,与用作匿名函数的借鉴自阿隆佐·邱奇的λ表示法[23],就可以构建出图灵完备的系统。麦卡锡提出的S-表达式,可以将程序与数据用相同的结构存储,这被称为同像性。Scheme的语法即来自S-表达式,这一特性使得在Scheme中实现自循环解释器变得非常简单[24]

在1973年,麻省理工学院Carl Hewitt英语Carl Hewitt提出的一种叫做演员模型计算模型[25],并用Lisp开发当时叫做Planner英语Planner (programming language)-73的新语言来实现它[26]。杰拉德·萨斯曼与盖伊·史提尔为了理解演员模型,决定在Maclisp工作环境中实现一个微型Lisp解释器,并接着增加创建演员和发送消息的机制[27]。两人很快发现了演员模型与λ演算之间的相似性,所谓“演员”就是彼得·兰丁提出的闭包[28],而Joel Moses英语Joel Moses在1970年已将它介入Lisp用来解决函数参数问题英语funarg problem[29];故而实现演员的关键,是将词法作用域介入到Lisp中[30]

在1975年,基于对λ演算表达能力的理解[31],杰拉德·萨斯曼与盖伊·史提尔开发出了新型Lisp解释器[32],并将在其上完成的微缩版的演员实现命名为“Schemer”,后因ITS英语Incompatible Timesharing System操作系统文件名的字符数限制而改为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实现要求英语Scheme Requests for Implementation(SRFI),以此为实际编程场合提供对R6RS标准的继续完善,Larceny和Chibi[3]提供了R7RS-Large过渡草案红色版(即数据结构部分)的全部SRFI的实现。

命名法

在正式场合比如Scheme标准的命名法英语Nomenclature中,提及一个λ表达式或原始过程时,偏好使用词语“过程”而非“函数”。在一般使用中,词语“过程”和“函数”是可互换使用的。过程应用有时被正式称呼为“组合”(combination)。

同其他Lisp一样,在Scheme中使用术语“thunk英语thunk”,来提及没有实际参数的过程。术语“适当”(proper)尾递归,称谓所有Scheme实现的一个性质,它们都进行尾递归优化,从而支持无限数目的活跃尾递归

基础特征

Scheme大体上是一个函数编程语言,并支持其他编程范型。它同其他Lisp编程语言家族语言共享了很多特征。它的非常简单的语法基于LispS-表达式:即圆括号包围的列表,其中的前缀是算符,而随后那些的是实际参数。故而Scheme程序由嵌套的列表的序列构成。列表也是Scheme中的主要数据结构,这导致了在原始码和数据格式之间的紧密等价性,即同像性。Scheme程序可以轻易的动态建立和求值Scheme代码片段。

Scheme与其他Lisp方言的列表,都是基于最基础的数据结构有序对(pair)。Scheme从它的Lisp先辈继承了一组丰富的列表处理原始运算,比如conscar英语CAR and CDRcdr英语CAR and CDR。Scheme的变量都使用动态强类型系统,Scheme中的过程都是头等物件,即头等函数,因此过程可以作为值赋值给变量,或作为实际参数传递给过程。

本章主要集中于语言的创新特征,它们使得Scheme区别于其他Lisp方言。除非专门指出,这里描述的特征都有关于R5RS标准。在本章例子中使用“===> 结果”表示法,来指示求值在紧前行上的表达式的结果。这同于R5RS中使用的约定。本章节描述的特征使得Scheme不同于在它之前的其他编程语言。Scheme的这些方面强烈的影响了Scheme语言的所有产品,并共通于始自1975年最初的λ论文,特别是自从R2RS以来[58],所有版本的Scheme编程语言。

极简主义

Scheme的简约性,使它成为具备同级别功能的编程语言中最易于实现的语言,这得益于使用λ演算来从更原始的形式导出多数的语言语法[59]。例如在R5RS标准中,定义了23个基于S-表达式的语法构造,其中14个被归类为派生形式或库形式,它们可以被写为涉及原则上为lambdad的更基础形式的宏。正如R5RS(§3.1)所说:“最基础的变量绑定构造是lambda表达式,因为所有其他的变量绑定构造,都可以依据lambda表达式来做出解释”[49]

基础形式definelambdaquoteifdefine-syntaxlet-syntaxletrec-syntaxsyntax-rulesset!
派生形式doletlet*letreccondcaseandorbegin、命名letdelayunquoteunquote-splicingquasiquote

下面是一个宏的例子,它将let实现为使用lambda来进行变量绑定的一个表达式:

(define-syntax let
  (syntax-rules ()
    ((let ((var expr) ...) body ...)
     ((lambda (var ...) body ...) expr ...))))

这里的(let ((var expr) ...) body ...)称为“模式”,模式中起始的let被称为“关键字”,syntax-rules ()的空括号中可添入作为辅助关键字的叫做“文字”的标识符,varexprbody称为“模式变量”,...是称为“省略号”的标识符,它表示所跟随的子模式可以匹配零或多个输入中的元素;而((lambda (var ...) body ...) expr ...)称为“模板”。使用上述定义的let,一个Scheme实现可以将(let ((a 1)(b 2)) (+ b a)),重写为((lambda (a b) (+ b a)) 1 2),这将实现任务缩减为编码过程实例化的工作。

词法作用域

不像更早期的Lisp比如Maclisp[60],Scheme像多数现代语言一样,采用了词法作用域[61]:在一个程序单元中所有可能的变量绑定,都可以通过阅读这个程序单元来分析出来,而不需要考虑到它可能在其中被调用的那些上下文。这对比于动态作用域,它是早期Lisp方言的特征,因为在当时的编译器和解释器中,用来实现词法作用域算法的原始的文字替换方法,关联着处理代价。在动态作用域的Lisp中,对一个过程内的自由变量的引用,依赖于这个调用的上下文,完全有可能引用到这个过程外部的相当不同的绑定。

λ演算

邱奇λ演算的数学表示法,启发了Lisp使用lambda作为介入一个过程的关键字,并影响了Lisp中涉及到使用高阶函数函数式编程技术的发展。但是早期的Lisp由于对自由变量的处理方式[62],而不适合表达λ演算[31]

一个正式的λ系统,拥有一些公理和一个完备的推理规则。这有助于使用数理逻辑和工具来进行分析。在这个系统中,演算可以被看作直接的演绎。λ演算的语法,来自用x, y, z, ...、圆括号、空格、点号和符号λ形成的递归表达式[63]。λ演算的功能包括:首先,充当强力的数理逻辑的起点。其次,它可以缩减编程者在考虑实现细节上的要求,因为它可以用于模拟机器求值。最后,λ演算建立了一个坚实的元理论[64]

在Scheme中,lambda关键字被用于定义匿名过程,并且使用define基础形式来定义命名过程,即将一个lambda过程绑定到指名的全局变量[65]。在Scheme中,(define (foo x) (+ x 1))(define foo (lambda (x) (+ x 1))) ,在语法上是等同的。例如有序对可以表示为[66]

(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)))

这样定义出来的conscarcdr,满足恒等式(car (cons x y))等于x,和(cdr (cons x y))等于y。甚至递归也可以表示为利用λ演算的Y组合子[67]

词法作用域的介入[61],通过在某些形式的λ表示法,和在工作编程语言中它们的实际表达之间做出等价,解决了早期Lisp的有关问题。Sussman和Steele展示了,通过将λ表达式用作“控制结构和环境修改器”,而非简单的过程实例化,可以用这个新语言优雅的导出其他编程语言包括ALGOLFortran的,所有指令式和声明式语义,和其他Lisp的动态作用域[68]。他们在第一篇λ论文中,与对Scheme的首次描述一起,介入了续体传递风格[69],并在后续的论文中,他们推进演示了在这种实际使用中体现出的λ演算的原生能力。

过程应用中的求值次序

Scheme采用了传值调用,但不同于多数Lisp规定了过程实际参数的求值次序,Scheme不规定求值次序。对比于其他Lisp,Scheme表达式在算符位置(第一个项目)上可以是一个表达式,只要在算符位置上的表达式的结果是一个过程,这种表示就是完全合法的[70]

在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是一个过程,它描述传递给它的实际参数,接着返回这个实际参数的值。在调用过程+来加12之中,表达式(ev +)(ev 1)(ev 2)可以按任何次序求值,只要效果上它们不像是并行求值的。因此在上述例子被执行的时候,标准Scheme可以按任何次序来显示前三行:

Evaluating procedure
Evaluating 1
Evaluating 2
3

然而一行的文本不可以同另一行的文本产生交错,因为这会违反顺序求值约束。

块结构

Scheme的代码块结构,不同于早先Lisp的语言构造[71]。自从R2RS开始[58],Scheme中的块,是通过三个“绑定构造”来实现的:let英语Let expressionlet*letrec。例如,下列构造建立一个,其中叫做var的符号被绑定到数10

(define var "goose")
;; 这里任何到var的引用都会被绑定到"goose"
(let ((var 10))
  ;; 语句走到这里时,任何到var的引用都会绑定到10
  )
;; 这里任何到var的引用都会绑定到"goose"

依据编程者的需要,块可以嵌套英语Nesting (computing)来建立任意复杂的块结构。使用块结构来建立局部绑定,减轻了不然可能发生的命名空间冲突英语Naming collision

let的变体之一let*,允许绑定引用在前面相同构造中定义的变量,例如:

(let* 
  ((var1 10)
   (var2 (+ var1 12)))
  ;; 但是var1的定义不可以引用var2
  )

let的另一个变体letrec,被设计用来确使互递归过程可绑定彼此。

下面的例子是侯世达雌雄序列英语Hofstadter sequence#Hofstadter Female and Male sequences

;; 将侯世达雌雄序列计算为有序对的列表
(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[72],但是在Scheme中更推崇的惯用法英语Programming idiom,是使用尾递归来表达迭代[73]。遵循标准的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中续体头等对象[74]。自从R2RS开始[58],Scheme提供了过程call-with-current-continuation英语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年提出的阴阳谜题[75],展示了Scheme可以将续体当作头等对象处理,绑定它们到变量,和把它们作为给过程的实际参数来传递[76]

统一的命名空间

对比于Common Lisp,在Scheme中所有的数据和过程共享一个共同的命名空间,而Common Lisp中有函数和数据分离的命名空间,使得一个函数和一个变量可以有相同的名字,并且将一个函数作为值引用时要求特殊的表示法。这有时叫做“Lisp1与Lisp2”差异,二者分别称谓Scheme的统一的命名空间,和Common Lisp的分立的命名空间[77]

在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[78]),和通过用#||#围绕文本,产生的“多行注释”或“块注释”。

在布尔表达式中非布尔值的处理

在多数Lisp方言包括Common Lisp中,布尔表达式中的值NIL按照惯例被求值为值假。在Scheme中,自从1991年的IEEE标准[48],除了#f的所有的值,包括在Scheme中写为'()NIL的等价者,在布尔表达式中求值为值真(R5RS sec. 6.3.1)[49]

在多数Lisp中表示布尔值真的常量是T,而在Scheme中它是#t

干净宏

Scheme的语法可以轻易的通过宏系统来扩展[79]。R5RS标准介入了强力的干净宏系统[80],它允许编程者使用一种简单的模式匹配子语言,向语言增加的新的语法构造(R5RS sec 4.3)[49]。在此前的R4RS标准中,干净宏系统已经作为“高级”系统,和“低级”宏系统一起被编排入标准的附录中[81],二者都被当作对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系统的非干净宏。

不能指定一个宏是否为干净的,是宏系统的一个缺点。可作为替代的展开模型比如作用域集合,提供一种潜在的解决方案[82]

延迟求值

自从R2RS开始[58],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永远只求值一次。它们可以被用来实现高级的惰性求值构造比如[83]

在R5RS中,给出了delayforce的推荐实现,将promise实现为没有实际参数的一个过程(thunk英语thunk),并使用记忆化来确保它永远只求值一次,与调用force的次数无关(R5RS sec. 6.4)[49]。在R6RS标准中,它们不再是原始运算,转而作为R5RS兼容库的一部分提供(rnrs r5rs (6))。

SRFI 41确使表达有限和无限序列能够具有非凡的经济性。例如,这是使用在SRFI 41中的函数定义的一个斐波那契数列[83]

;; 定义斐波那契序列
(define fibs
  (stream-cons 0
    (stream-cons 1
      (stream-map +
        fibs
        (stream-cdr fibs)))))

;; 计算这个序列的第一百个数
(stream-ref fibs 99)
===>  218922995834555169026

eval及其运行环境

R5RS标准之前,在其他Lisp中无处不在的eval过程,在Scheme中没有等价者。在第一篇λ论文中,曾经将evaluate描述为“类似于LISP函数EVAL”[84],但在具有词法作用域的Scheme中,对这个表达式在哪个环境中求值存在困惑。例如,不明确求值下列表达式的结果应当是5还是6[85]

(let ((name '+))
  (let ((+ *))
    (evaluate (list name 2 3))))

在求值实际参数name的时候,在外层环境中找到了它的定义;在求值结果表达式(+ 2 3)的时候,如果在外层环境中求值,结果是两个运算数的总和;如果在内层环境中求值,这里符号+已经被绑定到过程*的值,结果是两个运算数的乘积。为此在1978年的最初修订报告中,将evaluate替代为enclose,它接受分别为代码和运行环境的两个实际参数[86]。由于各种技术和实际原因,第二次、第三次和第四次修订报告省略了任何eval的等价者[85]

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->inexactinexact->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-portcurrent-output-port来访问,它们对应于Unix概念的标准输入和标准输出。多数实现还提供current-error-port。标准通过标准过程比如with-input-from-filewith-output-to-file,支持输入和标准输出的重定向。多数实现提供有重定向能力的字符串端口,使用在SRFI 6中描述的过程[87],确使很多常规的输入-输出运算能在字符串缓冲区上进行而非在文件上。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-charread-charpeek-charchar-ready?。为了书写和阅读Scheme表达式,Scheme提供了readwrite。在读运算时,如果输入端口到达了文件的结束处,则返回的结果是end-of-file对象,并且这可以使用谓词eof-object?来测试。

除了标准之外,SRFI 28定义了一个基本的格式化过程,类似Common Lisp的format并以此命名[88]

标准过程的重定义

在Scheme中,过程被绑定到变量[65]。在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”的形式被归类为标准中的派生的“库”形式,并且在实践中通常被实现为使用了更基础形式的宏,这使得实现任务比在其他语言中要容易许多。

More information 用途, 形式 ...
R5RS Scheme语言中的标准形式
用途 形式
定义 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)
Close

注意begin在R5RS中被定义为库语法,但是展开者需要知道它来完成分片功能。在R6RS中它不再是库语法。

标准过程

下面两个表格描述在R5RS Scheme中的标准过程。R6RS更加具有可扩展性而如此总结是不实际的。

某些过程出现在不止一行之中,因为它们不能轻易的分类入语言的一个单一功能。

More information 用途, 过程 ...
R5RS Scheme语言中的标准过程
用途 过程
构造 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
Close

在其名字中包含-ci的字符串和字符过程,在它们的实际参数之间进行不依赖大小写的比较:相同字符的大写和小写版本被认为是相等的。

More information 用途, 过程 ...
R5RS Scheme语言中的标准数值过程
用途 过程
基本算术算符 +, -, *, /, 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?
Close

接受多于二个实际参数的-/在R5RS中被定义了但留作为可选的。

Scheme实现要求

由于Scheme的极简主义,很多常见过程和语法形式不是由标准定义的。 为了保持核心语言很小并促进扩展的标准化,Scheme社区拥有“Scheme实现要求”(SRFI)流程,通过对扩展提案的仔细研讨来定义扩展库。这提升了代码可移植性。很多SRFI被几乎所有Scheme实现支持。

在不同的实现中具有相当广泛支持的SRFI包括[89]

  • 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的精简设计,使得编程语言设计人士与爱好者,特别钟爱研究它,故而它有不断涌现的新实现,而活跃开发的实现也在持续跟进语言标准更新[90]。尽管Scheme有众多实现是它的一个主要长处[21],但是由于Scheme没有库函数标准,造成了很多不可互通的实现互相竞争,试图使用Scheme编写既复杂又便于移植的程序,往往比较困难[22]。R6RS和R7RS-Large,在核心语言之外制定了库函数标准,使得编译器开发者和贡献者可以实现Scheme的可移植库。

几乎所有Scheme实现都有传承自Lisp的“读取﹣求值﹣输出循环”交互模式,一些Scheme实现亦可作为编译器。很多用C语言及派生语言写成的软件,都利用Scheme作为脚本语言,很多嵌入式系统语言即是基于Scheme。Chez SchemeLarceny等实现,可以将Scheme程序编译为本机二进制码。GambitChicken等实现,可将Scheme程序编译为C代码,Bigloo英语Bigloo还能编译成JVM字节码或者.Net字节码。

一些实现支持额外的特征。比如Kawa英语Kawa (Scheme implementation)提供与Java类的集成,而Scheme到C编译器通常易于使用C写成的外部库,甚至允许将实际C代码嵌入到Scheme原始码中。另一个例子是用java书写的Pvts英语Pvts,它提供了支持Scheme学习的一组可视工具。

教科书

实际用处

很多著名的电脑科学院校都利用Scheme来教授入门级课程。以下为一些最为著名的教授Scheme的学校:

自由软件影像处理程序GIMP利用Scheme为嵌入式脚本语言[96]GNOME中有到核心库的一个GNU的扩展语言Guile包装器[97]。在2012年出现的Julia所采用的语法解析器,是用Scheme方言Femtolisp实现的。

参见

注释

延伸阅读

外部链接

Wikiwand in your browser!

Seamless Wikipedia browsing. On steroids.

Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.

Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.