在人工智能语言中我们应该接触过Scheme。根据介绍,Scheme和CommonLisp是两种主要的Lisp方言之一,也就是说,在讲Lisp语言的时候,我们也包括了讲Lisp或者CommonLisp语言。Scheme语言的地位是由一系列的Lambda论文决定的。见https://en.wikipedia.org/wiki/History_of_the_Scheme_programming_language#The_Lambda_Papers。Scheme的后缀扩展名是.scm或者.ss。Scheme是动态强类型的语言,具有词法作用域,当然是函数式语言。此外,Scheme的特点是它是第一个实现了头等延续的语言(这里的延续指的是计算机程序的控制状态,头等延续指的是可以创建,保存、赋值程序的状态给一个变量,并根据需要恢复程序的运行上下文)。对于延续的介绍可见https://en.wikipedia.org/wiki/Continuation。
Scheme对于CommonLisp的设计产生了重要的影响。Scheme的开发其实是基于实现当时的Actor并发模型的思想。Scheme的最新标准是R7RS,制定于2013年。
描述Scheme的最佳的方式或许是这样的。首先,Scheme的语法风格来自于简洁的S-表达式。然后数据结构是基于表处理的,刚开始的时候我们会看到很多的表处理的模式(如果有可能的话,我们也可以在其它的编程语言中模仿它),这种表处理的模式导致了我们很容易在运行的时候动态地创建Scheme代码。另外就是,它支持头等函数。虽然声称Lisp是函数式语言,但是其实Lisp的基本思想其实并不是高阶函数,而是所谓的抽象重写系统。另外的特点,就是对于\(\lambda\)-演算和词法作用域的支持了。
语言核心中比较强大的应该是Scheme与Lisp语言中的Hygienic macro,也就是所谓的干净的宏了。这里的干净是指宏的名词不会引起词法作用域的冲突。最早的关于干净的宏的文章见于Kohlbecker在1986年的文章《Hygienic Macro Expansion》。现在看来,TeX系统中用的宏与Lisp语言中的差不多。它们都是词法意义上的,虽然行为上与惰性求值类似,但是其实不是一回事。使用宏的系统更像是基于规则的系统。Perl6与Julia语言也实现了像Lisp那样的宏。Elixir与Dylan语言中也可以。看来,我们得认真地对待Lisp的宏了。
对GNU Guiler的介绍[09-01-2015 09:59:38 CST]
GNU Guile是GNU的一个Scheme语言的实现。我们知道,一个程序设计语言可以使用不同的后端。比如JBC(Java Bytecode)可以由Scheme、Python等语言的前端来生成。Guile最早的版本是在1993年发布的。相对于Scheme规范添加了许多的模块化扩展。
guile提供了libguile,用于支持将语言嵌入到其它的语言中(通过C API)。另外,使用C语言开发的许多的程序同时也可以移植到guile中。因为语言本身就是与后端的实现没有关系的。而在前端中嵌入代码又是十分正常的。guile语言的全名是GNU Ubiquitous Intelligent Language for Extensions。也就是说,本来的设计就是为了扩展的。实际上,该语言也被嵌入在GnuCash与Lilypond等语言中。
Guile系统实现的是所谓的R5RS,同时具有大部分的R6RS中的功能特性。SRFI的功能特性也具有一些。更多的是自己的扩充。目前有种将Emacs的脚本语言换成Scheme的计划。也许之后我们就可以比较自如地进行Scheme上面的Emacs编程了。
目前的几个Scheme实现,大多也是实现了R6RS,对于R7RS的支持都比较有限。所以我们目前还是以R6RS的标准为根据。下面我们打算介绍一下这个标准。R6RS有几个相关的标准,不只是核心语言,也包括一些库。但是Scheme的标准是出了名的精简。即使我们花时间全部读完成是值得的。
Scheme规范R6RS
导引的部分是这样的,首先Scheme实现了一等过程。也就是说,过程就像一个真正的值一样。注意,Scheme不是纯函数式的语言,所以它的函数解释为过程也非常自然。说是一等过程而不是一等函数还是比较自然一些。Scheme was one of the first programming language to incorporate first-class procedures as in the lambda calculusd, there by proving the usefulness of static scope rules and block structure in a dynamically typed language.
接下来我们要解释一下宏的概念。其实在函数的定义中,就是惰性的。求值发生在函数调用的时候。但是对于宏而言,其实是按名求值。在调用宏的时候,是把宏体做一个展开。TeX语言也是这样的一种模式。其实宏大概只是一种符号计算的方式而已。使用宏带来的一个明显的好处是我们可以方便地操纵程序语言的代码。
对Scheme语言的基本的描述
R6RS的第一节介绍的是其语义。也就是说,把词法与语法甚至放到无关紧要的位置。首先是按照Algol的语言特征,Scheme实现的是静态作用域。使用变量的时候,变量关联到的时在词法上与该变量相绑定的位置。
Scheme的类型是属于“manifest types”。也就是说,类型是与对象(或者称为值)绑定的,而不是变量。具有类型的是特定的值而不说变量属于某个类型。所以经常有人说Scheme是弱类型的语言甚至说它是无类型的语言。其实Python、Ruby、Smalltalk也是属于manifest types。与Scheme不同的是Haskell、Java、ML等强类型的语言。
所以这给我们提了一个醒,最好不要单纯地使用无类型与有类型来描述编程语言,也不使用强类型与弱类型来描述程序设计语言。最清晰的方法是使用宣告式类型或者隐式类型。其实类型化有一些专门的术语的,参考<https://en.wikipedia.org/wiki/Manifest_typing>我们可以知道manifest typing、latent typing、implicit typing、dynamic typing、subtyping等。
Scheme有自己的废料收集机制,所以所涉及的对象也不用在语言中显式地销毁。看过《编程原本》之后我们应当知道对象、实体、类型、值的区别。这里的Object的含义应该是跟C++中的是一样的。也就是说,对象是对占据特定存储的那种事物的抽象。在这一点上,Scheme与Python、Haskell、Java都是相同的。
另一个特点是过程作为一等对象。过程可以动态地创建,并且保存在特定的数据结构中。这一点与Haskell、ML、Ruby、Smalltalk都是相同的。也就是说动态创建过程的能力与它们是相同的。
Scheme特有的概念是所谓的一等延续。也就是特定的程序上下文可以保存下来。在执行过程调用之前,过程的参数会被积极求值。也就是说如果我们把一个表达式作为参数传递给了一个过程,那么是把这个表达式的结果传递给这个过程。C、Python、Ruby在内的多种语言都使用这种求值机制。Haskell与R语言是按需调用。也就是说是否对传入的表达式求值由子过程来决定。
Scheme的对象与值
Scheme的对象与值被组织成类型(而变量没有类型)。基本的类型有布尔型、数值、字符串、字符、符号、列表、数对等。使用#f和#t分别表示True和False。
之前我们读到过Scheme的设计原理。在数据类型中,Pair是最基本的元素。甚至List都是由Pair构成的。知道\(\lambda\)-演算的人都知道组合子,也就知道如何用组合子来表示真与假。
Scheme的表达式
表达式是Scheme的最重要的组成元素了。表达式求值之后得到一个value。其实表达式可以建模成一种抽象数据类型的。比如,表示值的那些字符串是一个表达式,对它们求值得到一个值;表达式可以有子表达式等。Scheme的表达式采用前缀记法。
在语法上,换行符并不会影响表达式的解析。这主要是为了编程的考虑。
变量、绑定、定义、过程
let用于声明一个局部的变量的过程。如
(let ( (x 23) (y 42) ) (+ x y))上面的表达式中,let的第一个参数是对于符号的定义的列表,第二项是相应的表达式。结果是按照赋的值求解第二个表达式。注意这里的变量是局部定义的。
如果要让变量的定义超过当前的作用域,需要使用define。define是一个定义而不是一个表达式,因为它不返回任何的值,并且只能在程序的顶层出现。
(define x 23)
(define y 42)
(+ x y)在一个嵌套的环境中内层的符号与哪个值绑定当然是按照最邻近的原则。另外,按照词法作用域的准则。求值的结果是依赖于定义的环境而不是被调用的环境。
过程也使用define来定义。
(define (f x)
(+ x 42))
(f 23)在定义的时候,函数及其参数作为第二个参数,而后面定义的是函数的体。过程是与特定的对象有关的表达式的抽象。由于后面的体可以看成是一个宏体,所以我们可以非常方便地定义高阶函数:
(define (apply_binop binop first second)
(binop first second))Scheme中虽然广泛采用了模式匹配。但是有些情况下的模式匹配仍然是会失败的,比如如下的代码:
(* 失败的代码*)
(define (apply_binop binop (first second))
(binop first second))lambda表达式用于创建新的过程,过程中还可以包含闭包
((lambda (x) (+ x 42)) 23)上面的表达式都是过程调用,但是let与lambda开头的却不是过程调用。这是因为let与lambda并不是函数名称,而是Scheme语言的关键字。define也同样是如此。关键字后面跟的参数遵守怎样的规则是Scheme语言定义的。
宏与模式匹配
在Scheme中,宏是声明具有特定的模式的参数的方法。借助于宏我们可以实现与lambda、define一样的效果。定义宏使用的是define-syntax语句。
(define-syntax def
(syntax-rules ()
((def f (p ...) body)
(define (f p ...)
body))))
(def f (x) (+ x 42))上面的语法将使def成为一个具有和define相同的效果的关键字。上面的语句的理解方式是,define-syntex定义了一个语法糖def,这个语法糖的使用规则由syntax-rules语句来声明。syntex-rules指出,这个语法糖模式匹配(def f (p ...) body)。匹配到这个语法的时候,执行的动作是上面的define。
1.10引用与阻止求值
在介绍这一节的时候,感觉使用'表示阻止求值这种理解方式非常怪异。显得没有必要。但是其实把Scheme当成是一种自然语言就好了。这里的阻止求值其实就相当于自然语言中的引号。我们参考Quasi-quotation就知道了。既然它们是引号,自然里面的东西就不会再发生改变了。
'23
(quote 23)在通常语言里,括号里面的东西就是字符串。但是在Scheme中,括号里面的东西仍然是保持Scheme语言里面的结构的。所以虽然说是一种引用,但是括号里面的东西不是一堆字符串,而是Scheme中的语法结构。
1.12库机制与顶层调用
Scheme中的库使用library关键字。库里面的内容可以主动导出与主动导入。
(library (hello)
(export hello-world)
(import (rnrs base) (rnrs io simple))
(define (hello-world)
(display "Hello World")
(newline)))Scheme的顶层程序第一行可以加一个Shell命令。以#!开头。
Scheme中的词法
在Scheme中语法定义是非常简单的。大约只有一页多。如
(define 加 +)
(加 1 2)
(define 真 #t)
(define 假 #f)
(define 非 not)
(非 真)这样可以把scheme真接改造成一个中文编程环境。我们需要注意的,是Scheme中的quote与quasi-quote的机制。因为这一机制在其它的语言中很少见(或者说不常见)。