大多数计算机语言都有许多语法规约(例如关键字、中缀操作符、括号、操作符优先级、点标记、分号等等),但是,作为Lisp语言家族中的一员,Scheme所有的语法都是基于包含在括号中的、采用前缀表示的列表的。这种表示看起来似乎有些陌生,但是它具有简单一致的优点。(一些人戏称”Lisp”是”LotsofIrritatingSillyParentheses“――“大量恼人、愚蠢的括号“――的缩写;我认为它是”LispIsSyntacticallyPure“――“Lisp语法纯粹”的缩写。)考虑下面这个例子:
/*Java*/if(x.val()>0){z=f(a*x.val()+b);}/*Scheme*/(if(>(valx)0)(set!z(f(+(*a(valx))b))))注意上面的惊叹号在Scheme中并不是一个特殊字符;它只是”set!“这个名字的一部分。(在Scheme中)只有括号是特殊字符。类似于(set!xy)这样以特殊关键字开头的列表在Scheme中被称为一个特殊形式(specialform);Scheme的优美之处就在于我们只需要六种特殊形式,以及另外的三种语法构造――变量、常量和过程调用。
在该表中,var必须是一个符号――一个类似于x或者square这样的标识符――number必须是一个整型或者浮点型数字,其余用斜体标识的单词可以是任何表达式。exp…表示exp的0次或者多次重复。
更多关于Scheme的内容,可以参考一些优秀的书籍(如[Friedman和Fellesein
**语言解释器的职责**一个语言解释器包括两部分:
2、执行:程序的内部表示(由解释器)根据语言的语义规则进行进一步处理,进而执行源程序的实际运算。(Lispy的)执行部分由eval函数实现(注意,该函数覆盖了Python内建的同名函数)。
下面的图片描述了解释器的解释流程,(图片后的)交互会话展示了parse和eval函数对一个小程序的操作方式:
这里,我们采用了一种尽可能简单的内部表示,其中Scheme的列表、数字和符号分别使用Python的列表、数字和字符串来表示。
执行:eval下面是eval函数的定义。对于上面表中列出的九种情况,每一种都有一至三行代码,eval函数的定义只需要这九种情况:
defeval(x,env=global_env):"Evaluateanexpressioninanenvironment."ifisa(x,Symbol):#variablereferencereturnenv.find(x)[x]elifnotisa(x,list):#constantliteralreturnxelifx[0]=='quote':#(quoteexp)(_,exp)=xreturnexpelifx[0]=='if':#(iftestconseqalt)(_,test,conseq,alt)=xreturneval((conseqifeval(test,env)elsealt),env)elifx[0]=='set!':#(set!varexp)(_,var,exp)=xenv.find(var)[var]=eval(exp,env)elifx[0]=='define':#(definevarexp)(_,var,exp)=xenv[var]=eval(exp,env)elifx[0]=='lambda':#(lambda(var*)exp)(_,vars,exp)=xreturnlambda*args:eval(exp,Env(vars,args,env))elifx[0]=='begin':#(beginexp*)forexpinx[1:]:val=eval(exp,env)returnvalelse:#(procexp*)exps=[eval(exp,env)forexpinx]proc=exps.pop(0)returnproc(*exps)isa=isinstanceSymbol=streval函数的定义就是这么多…当然,除了environments。Environments(环境)只是从符号到符号所代表的值的映射而已。一个新的符号/值绑定由一个define语句或者一个过程定义(lambda表达式)添加。
让我们通过一个例子来观察定义然后调用一个Scheme过程的时候所发生的事情(lis.py>提示符表示我们正在与Lisp解释器进行交互,而不是Python):
lis.py>(definearea(lambda(r)(*3.141592653(*rr))))lis.py>(area3)28.274333877当我们对(lambda(r)(*3.141592653(*rr)))进行求值时,我们在eval函数中执行elifx[0]=='lambda'分支,将(_,vars,exp)三个变量分别赋值为列表x的对应元素(如果x的长度不是3,就抛出一个错误)。然后,我们创建一个新的过程,当该过程被调用的时候,将会对表达式['',3.141592653['','r','r']]进行求值,该求值过程的环境(environment)是通过将过程的形式参数(该例中只有一个参数,r)绑定为过程调用时所提供的实际参数,外加当前环境中所有不在参数列表(例如,变量*)的变量组成的。新创建的过程被赋值给global_env中的area变量。
那么,当我们对(area3)求值的时候发生了什么呢?因为area并不是任何表示特殊形式的符号之一,它必定是一个过程调用(eval函数的最后一个else:分支),因此整个表达式列表都将会被求值,每次求值其中的一个。对area进行求值将会获得我们刚刚创建的过程;对3进行求值所得的结果就是3。然后我们(根据eval函数的最后一行)使用参数列表[3]来调用这个新创建的过程。也就是说,对exp(也就是['',3.141592653['','r','r']])进行求值,并且求值所在的环境中r的值是3,并且外部环境是全局环境,因此*是乘法过程。
现在,我们可以解释一下Env类的细节了:
classEnv(dict):"Anenvironment:adictof{'var':val}pairs,withanouterEnv."def__init__(self,parms=(),args=(),outer=None):self.update(zip(parms,args))self.outer=outerdeffind(self,var):"FindtheinnermostEnvwherevarappears."returnselfifvarinselfelseself.outer.find(var)注意Env是dict的一个子类,也就是说,通常的字典操作也适用于Env类。除此之外,该类还有两个方法,构造函数__init__和find函数,后者用来为一个变量查找正确的环境。理解这个类的关键(以及我们需要一个类,而不是仅仅使用dict的根本原因)在于外部环境(outerenvironment)这个概念。考虑下面这个程序:
(definemake-account(lambda(balance)(lambda(amt)(begin(set!balance(+balanceamt))balance))))(definea1(make-account100.00))(a1-20.00)每个矩形框都代表了一个环境,并且矩形框的颜色与环境中最新定义的变量的颜色相对应。在程序的最后两行我们定义了a1并且调用了(a1-20.00);这表示创建一个开户金额为100美元的银行账户,然后是取款20美元。在对(a1-20.00)求值的过程中,我们将会对黄色高亮表达式进行求值,该表达式中具有三个变量。amt可以在最内层(绿色)环境中直接找到。但是balance在该环境中没有定义:我们需要查看绿色环境的外层环境,也就是蓝色环境。最后,+代表的变量在这两个环境中都没有定义;我们需要进一步查看外层环境,也就是全局(红色)环境。先查找内层环境,然后依次查找外部的环境,我们把这一过程称之为词法定界(lexicalscoping)。Procedure.find负责根据词法定界规则查找正确的环境。
剩下的就是要定义全局环境。该环境需要包含+过程以及所有其它Scheme的内置过程。我们并不打算实现所有的内置过程,但是,通过导入Python的math模块,我们可以获得一部分这些过程,然后我们可以显式地添加20种常用的过程:
defadd_globals(env):"AddsomeSchemestandardprocedurestoanenvironment."importmath,operatorasopenv.update(vars(math))#sin,sqrt,...env.update({'+':op.add,'-':op.sub,'*':op.mul,'/':op.div,'not':op.not_,'>':op.gt,'<':op.lt,'>=':op.ge,'<=':op.le,'=':op.eq,'equal':op.eq,'eq':op.is_,'length':len,'cons':lambdax,y:[x]+y,'car':lambdax:x[0],'cdr':lambdax:x[1:],'append':op.add,'list':lambda*x:list(x),'list':lambdax:isa(x,list),'null':lambdax:x==[],'symbol':lambdax:isa(x,Symbol)})returnenvglobal_env=add_globals(Env())PS1:对麦克斯韦方程组的一种评价是“一般地,宇宙间任何的电磁现象,皆可由此方程组解释”。AlanKay所要表达的,大致就是Lisp语言使用自身定义自身(Lispwas“definedintermsofLisp”)这种自底向上的设计对软件设计而言具有普遍的参考价值。――译者注
解析(Parsing):read和parse
接下来是parse函数。解析通常分成两个部分:词法分析和语法分析。前者将输入字符串分解成一系列的词法单元(token);后者将词法单元组织成一种中间表示。Lispy支持的词法单元包括括号、符号(如set!或者x)以及数字(如2)。它的工作形式如下:
>>>program="(set!x*2(*x2))">>>tokenize(program)['(','set!','x*2','(','*','x','2',')',')']>>>parse(program)['set!','x*2',['*','x',2]]有许多工具可以进行词法分析(例如MikeLesk和EricSchmidt的lex)。但是我们将会使用一个非常简单的工具:Python的str.split。我们只是在(源程序中)括号的两边添加空格,然后调用str.split来获得一个词法单元的列表。
2将会在编译时被认定是错误。另一方面,Java和Python并不需要在编译时检测出表达式x/0是一个错误,因此,如你所见,一个错误应该何时被识别并没有严格的规定。Lispy使用read函数来实现parse函数,前者用以读取任何的表达式(数字、符号或者嵌套列表)。
tokenize函数获取一系列词法单元,read通过在这些词法单元上调用read_from函数来进行工作。给定一个词法单元的列表,我们首先查看第一个词法单元;如果它是一个')',那么这是一个语法错误。如果它是一个'(‘,那么我们开始构建一个表达式列表,直到我们读取一个匹配的')'。所有其它的(词法单元)必须是符号或者数字,它们自身构成了一个完整的列表。剩下的需要注意的就是要了解'2‘代表一个整数,2.0代表一个浮点数,而x代表一个符号。我们将区分这些情况的工作交给Python去完成:对于每一个不是括号也不是引用(quote)的词法单元,我们首先尝试将它解释为一个int,然后尝试float,最后尝试将它解释为一个符号。根据这些规则,我们得到了如下程序:
defread(s):"ReadaSchemeexpressionfromastring."returnread_from(tokenize(s))parse=readdeftokenize(s):"Convertastringintoalistoftokens."returns.replace('(','(').replace(')',')').split()defread_from(tokens):"Readanexpressionfromasequenceoftokens."iflen(tokens)==0:raiseSyntaxError('unexpectedEOFwhilereading')token=tokens.pop(0)if'('==token:L=[]whiletokens[0]!=')':L.append(read_from(tokens))tokens.pop(0)#popoff')'returnLelif')'==token:raiseSyntaxError('unexpected)')else:returnatom(token)defatom(token):"Numbersbecomenumbers;everyothertokenisasymbol."try:returnint(token)exceptValueError:try:returnfloat(token)exceptValueError:returnSymbol(token)最后,我们将要添加一个函数to_string,用来将一个表达式重新转换成Lisp可读的字符串;以及一个函数repl,该函数表示read-eval-print-loop(读取-求值-打印循环),用以构成一个交互式的Lisp解释器:
defto_string(exp):"ConvertaPythonobjectbackintoaLisp-readablestring."return'('+''.join(map(to_string,exp))+')'ifisa(exp,list)elsestr(exp)defrepl(prompt='lis.py>'):"Aprompt-read-eval-printloop."whileTrue:val=eval(parse(raw_input(prompt)))ifvalisnotNone:printto_string(val)下面是函数工作的一个例子:
>>>repl()lis.py>(definearea(lambda(r)(*3.141592653(*rr))))lis.py>(area3)28.274333877lis.py>(definefact(lambda(n)(if(<=n1)1(*n(fact(-n1))))))lis.py>(fact10)3628800lis.py>(fact100)93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000lis.py>(area(fact10))4.1369087198e+13lis.py>(definefirstcar)lis.py>(definerestcdr)lis.py>(definecount(lambda(itemL)(ifL(+(equalitem(firstL))(countitem(restL)))0)))lis.py>(count0(list012300))3lis.py>(count(quotethe)(quote(themorethemerrierthebiggerthebetter)))4Lispy有多小、多快、多完备、多优秀?
我们使用如下几个标准来评价Lispy:
bash$grep"^\s*[^#\s]"lis.py|wc903983423*高效:Lispy计算(fact100)只需要0.004秒。对我来说,这已经足够快了(虽然相比起其它的计算方式来说要慢很多)。
*优秀:这一点需要读者自己确定。我觉得,相对于我解释Lisp解释器这一目标而言,它已经足够优秀。
真实的故事
了解解释器的工作方式会很有帮助,有一个故事可以支持这一观点。1984年的时候,我在撰写我的博士论文。当时还没有LaTeX和MicrosoftWord――我们使用的是troff。遗憾的是,troff中没有针对符号标签的前向引用机制:我想要能够撰写“正如我们将要在@theoremx页面看到的”,随后在合适的位置撰写”@(settheoremx\n%)”(troff寄存器\n%保存了页号)。我的同伴,研究生TonyDeRose也有着同样的需求,我们一起实现了一个简单的Lisp程序,使用这个程序作为一个预处理器来解决我们的问题。然而,事实证明,当时我们用的Lisp善于读取Lisp表达式,但在采用一次一个字符的方式读取非Lisp表达式时效率过低,以至于我们的这个程序很难使用。
在这个问题上,Tony和我持有不同的观点。他认为(实现)表达式的解释器是困难的部分;他需要Lisp为他解决这一问题,但是他知道如何编写一个短小的C过程来处理非Lisp字符,并知道如何将其链接进Lisp程序。我不知道如何进行这种链接,但是我认为为这种简单的语言编写一个解释器(其所具有的只是设置变量、获取变量值和字符串连接)很容易,于是我使用C语言编写了一个解释器。因此,戏剧性的是,Tony编写了一个Lisp程序,因为他是一个C程序员;我编写了一个C程序,因为我是一个Lisp程序员。