Practical Common Lisp学习笔记——之第4章语法和语义

4.语法和语义

 

S-expressions

s-expression的基本元素是表list和原子atom。表是括号和里面包含用任意空白隔开的元素。原子则是除此之外的。一个表的元素自身也是一个s-expression。从技术上讲注释comment不是s-expression,由分号;开头,后面整行都被当做是空白。

 

数字是一串数,可以有符号sign(+-),包含小数点decimal point(.)或者斜线solidus(/),或者由指数标记结束。-2//8(比例)或者286/2-1/4123是一样的,1.01.0e0也是一样的(都是单精度浮点数)。但是,1.01.0d0(双精度浮点数)1是不同的,因为不同的浮点精度和整数类型不同。

 

字符串string是在双引号内部。在反斜杠backslash(\)转义escapes后一个字符,会不关它是什么都被包含在字符串内。只有两个字符必须转义符,双引号和反斜杠。所有其他字符都会忽略它的意思,被包含在字符串内。

 

Lisp程序里面的名字都被叫做符号symbol。基本上所有的字符都是合法的命名字符,作为隔断表元素的空白符whitespace不行。数字digits可以但是要保证名字整体不被解释为数字。也可以包含句号peridos,但是只有句号也不行。10个句法的保留字符不可以:正括号(,反括号),双引号,单引号,反引号`,逗号,,冒号:,分号;,反斜杠\,竖线|。他们甚至也可以用在命名中,用反斜杠\转义,或者用竖线|包裹字符转义。(REPL试的结果,如果定义函数\b-add,会被读取为|b-ADD|,调用时可以用|b-ADD|或者\b-add|,.:’|会被读为|,.:’|,而\,\.\\会被读为|,.\\|调用时候和前面两个一样。在调用时候同样会按照定义时候的方法读取名字。)

 

为了保证同样的名字,总是被读取为同样的符号,读取器reader会把所有没转义的字符,转换成大写的同样字符。如此,fooFooFOO都会被读取为同样的符号:FOO。但是\f\o\o|foo|都会被读取为foo。这就是为什么在REPL中定义个函数时,显示出的名字是大写的。最近标准的风格是,写代码的时候用小写,让reader把它转换为大写(不过实际上这个转换是可以自定义的。)(可以说CL是大小写不敏感的。)

 

为了保证同样原文的名字被读为同样的符号,reader保留interns符号,在把读取的名字转换为大写后,reader在一个表(叫package)中对现有的字符查找是否有相同名字,如果没有就创建一个新的符号添加进去。否则reader返回已存在于表中的符号。这样,在任何地方的s-expression中的相同名字,都描述同一个对象。

 

Lisp的命名规范与源于Algol的语言有些不同,Lisp里面允许更多字符。还有对全局变量global variable的命名是以*开头和结尾的。类似的,对常量constant的命名是以+开头和结尾。有些程序员对特别低级函数用%甚至是%%开头。在语言的标准规范里面定义的名字,用的字符只有(A-Z)加上*+-/12<=>&

 

S-expression作为Lisp Form

s-expression被求值为Lisp code,但并不是所有的s-expression都可以被读取器读取,被求值为Lisp codeCommon Lisp的求值规则定义了第2层语法,决定哪些s-expression可以被看作是Lisp form。这一层的句法很简单。任何原子(非表或者空表)就是合法的Lisp form。一个符号做为第1个元素的任何表也是合法Lisp form形式。

 

最简单的Lisp form原子atom可以被分成两类:符号symbol和其他的。一个符号被当作form求值,被当作是一个变量variable的名字,计算变量的当前值。第6章再说变量的初始值。常量constant variable或许有点矛盾,但实际上,符号PI是一个常量的名字,它的值最可能是浮点数,是数学常量pi的近似值。

 

所有其他的原子,数字和字符串时自求值self-evaluating对象。

 

符号symbol的名字做为符号自身值的时候,符号可以也是自求值。TNIL两个常量就是这样的方式定义的,表示真true和假false值。

 

另一类自求值符号,符号的名字以 :开头的关键字keyword符号,当读取器保留这样的名字时候,自动以该名字定义1个常量并用该符号做为它的值。

 

所有合格的lisp form都以一个符号symbol开头,但是3lisp form3种不同方式求值。为了做区分,求职器evaluator需要判断第1个符号是:函数名,宏,特殊操作符。如果符号还没有被定义会被当做是一个函数(可能的情况是在编译时候所引用的函数在稍后被定义)。

 

函数调用

函数调用的规则很简单:把表中余下的元素也作为Lisp form求值,然后把结果传给该名字的函数。这包含了一些句法限制:第1个元素以后的所有元素都是合法的Lisp form。基本的函数调用语法像后面的,每个参数本身也是Lisp form

 

       (function-name argument*)

 

表达式(+ 1 2)的求值过程:先对1求值,然后对2求值,然后,把结果传给+函数,返回3。复合的表达式(* (+ 1 2) (- 3 4))求值过程相似,先对参数(+ 1 2)(- 3 4)求值,进而需要先求值它们的参数,然后把结果传给它们对应的函数。最后,3-1被传给*函数,得到结果-3

 

特殊操作符

不只所有操作都可以被定义为函数,因为所有参数都会在被传递给函数前求值。

 

       (ifx (format t "yes") (format t "no"))

 

如果IF是函数,被从左至右的求值,(format t "yes")求值后产生结果NIL,然后,(format t "no")求值产生结果NIL,也就不能控制FORMAT表达式的求值。

 

Common Lisp定义了几十个特殊操作符,IF是其中一个,总共有25个,但是只有少数会在日常中用到。

 

如果一个表的第1个元素是特殊操作符,后面的表达式会按照操作符的规则求值。

 

IF的规则很简单:求值第1个表达式,如果结果不是非NIL,就求值后面一个表达式并返回它的值。否则,对第3个表达式求值并返回值。如果第3个表达式被忽略,就返回NILIF表达式的基本形式:

 

       (iftest-form the-form [else-form ])

 

操作符QUOTE也很简单,以一个单独的表达式作为“参数”不对他求值简单的返回它。像下面的使用,结果是表(+ 1 2)而不是3

 

(quote (+ 1 2))

 

返回的表没什么特别的,可以像用LIST函数创建出来的表一样使用。

 

(quote (+ 1 2))

 

也可以被写成:

 

       '(+1 2)

 

这个有点扩展的s-expression可以被读取器理解,上面两个表都会被看作是以QUOTE是第1个元素,表(+ 1 2)是第2个元素。

 

特殊操作符specialoperator是语言的特性,需要求值器进行一些特殊处理。很多特殊操作符控制环境以不同的形式进行求值。后面第6章会讨论LET,创建新变量的绑定。下面的例子求值结果是10,因为第2x的求值环境,已经由LET确定变量的值是10

 

 

(let ((x 10)) x)

 

宏是语言扩展语法的一种方法。对宏macro form的求值分两个阶段进行:首先,传递给宏的元素,把form中的元素,以非求值的方式,传递给宏函数macro function。其次,宏函数——调用它的展开expansion——返回的表form按照普通的求值规则求值。(就是把传递给宏的表,先由宏展开返回后,再求值。展开和返回展开结果都是在宏表达式的原地进行的。)

 

要清楚的记住这两个阶段,是分开进行的。虽然在REPL中这两个步骤是紧接着完成的,并且第2步的值马上就返回了。但是,如果编译了Lisp code,它的宏的展开是在编译的时候完成的,而最后的求值是在文件被加载后。COMPILE-FILE函数会把文件里面的所有宏都展开,直到只包含函数表和特殊操作符为止。这种去掉宏的代码编译成FASL文件可以被LOAD函数加载。宏在编译的时候展开就不会有在文件加载和函数调用时候运行时的开销,所以可以做很多生成展开工作。

 

因为求值器不会对传递给宏函数之前的宏表中的元素求值,所以传递给宏的form可以不是合法的Lispform。每个宏都会按照它自身的语法,用s-expression生成表达式。

 

后面会更多讨论宏,要认识到宏——语句构造上和函数调用类似——服务于很多不同的目的,提供了一种钩住hook编译器的方式。

 

TruthFalsehood,和Equality

Common Lisp里面用符号NIL表示假,其他所有的都是真。符号T是为了在函数返回非假植时更方便。NIL和空表()是等价的(这和C/C++里面用0表示false习惯一样),在读取器里面决定的,读取器读到NIL或者()时,都会视为是符号NIL。之前提到过NIL是一个常量它的值就是它本身符合NIL。表达式nil()'nil'()都会被求值为符号NIL。没有引号的是引用常量它的值就是符号NIL,带有引号QUOTE特殊操作符的会直接求值为符号。同样的t't也会求值为同样的符号T

 

如同用"thesame thing"来回避2个值的"the same"含义一样。在后面的章节会看到Common Lisp提供许多特殊类型的等式谓词:=用来比较数,CHAR=比较字符等。这部分我会讲述4个“通用”等式谓词——对任意2个相等的Lisp对象会返回true否则返回false。按判断强度(由严格的相等到宽松的相等)的顺序他们分别是,EQEQLEQUAL,和EQUALP

 

EQ检测“对象恒等”"object identity"——如果2个对象完全相同(对应C/C++应该是检测是否相同的内存地址)那么他们是EQ的。不幸的是,数和字符的值恒等依赖于该数据类型在特定Lisp中的实现(可能是指数和字符的内存表达方式)。因此,EQ可能认为同样值的2个数或者2个字符是相等的,或者可能不相等。实现有足够的余地对(eq 3 3)表达式能合法的求值为true或者false(这里如果出现不相等的情况可能是编译时一个值的常量可能有多个地址)。更重要的是,在(eq x x)中如果x的值碰巧是数或者字符也能求值得到true或者false

 

因此,你绝不应该用EQ比较可能是数或者字符的值。在特定的实现中它可能看上去能对确定的值执行可预测的行为,但是你不能保证它在切换了实现后也按同样方式执行。并且切换实现也可能只是简单的把实现更新到新版本——如果你的Lisp实现改变了如何描述数和字符,那么EQ的行为也会很好的同样改变。

 

于是,Common Lisp定义了类似EQ行为的EQL,同样保证2个相同类型的对象表现出相同的数字或者字符值是相等的。于是,(eq 1 1)保证是true。并且(eq 1 1.0)保证是false因为为整数值1和浮点数值是不同的类型实例。

 

对什么时候用EQ什么时候用EQL2个学派的观点:“尽可能使用EQ "use EQ when possible"一方主张你应该在知道除比较数或者字符之外都使用EQ因为(a)这是暗示你不是比较的数或字符的一种方式(b)会稍微提高效率因为EQ不会效验参数是否为数或字符。

 

“总是使用EQL"always useEQL"的一方说你绝不应使用EQ因为(a)潜在的好处却让清晰度降低因为任何时候某个人读你的代码——包括你自己——看到一个EQ,他们都会立即停下来检查是否正确使用(也就是说,它没有用在比较数或字符上面)(b)谣传EQEQL的效率差异是真的性能瓶颈。

 

本书的代码是“总是使用EQL”的风格。

 

另外2个等式谓词,EQUALEQUALP,一般对所有类型的对象都可以比较,但是它们比EQEQL宽松。他们分别定义了比EQL稍微广义的相等概念,允许不同对象视为是相等的。它们的功能实现上没有任何特殊的相等概念只是它们被过去的Lisp程序员发现更方便使用(估计是指的命名来源多了个L=less。如果这些谓词不满足你的需求,你也可以定义你自己的谓词函数,按你的需要比较不同类型的对象。

 

EQUAL对相同结构和内容的,递归的符合EQUAL判别的表list认为相等。EQUAL对有相同字符的字符串string认为相等。对后面章节会讨论的2个数据类型,位向量bit vector和路径名pathname的判别也比EQL要求更低。对其他所有类型都退回到EQL的方式

 

EQUALPEQUAL相似但是要求更低。它对字符串的比较会忽略掉大小写(大小写不敏感)。同样的对字符的比较也忽略大小写。对数学上相同值的数认为是相同的。于是,(equalp 1 1.0)true。表list满足EQUALP的时候它的元素也是满足EQUALP的;同样的数组array满足EQUALP的时候元素也是满足EQUALP的。EQUALP对其他一些没提及的数据类型的2个对象相等判断时会回退到EQL

 

格式化Lisp代码

从严格意义上讲代码的格式化,对语法和语义没太打影响,而是增加代码的可读性和写的时候方便。Lisp代码的格式化关键是适当的排版缩进。缩进应该反映出代码的结构,而不需要数有多少对括号哪2个是配对的(用缩进表现出像C/C++一样的代码块结构)。一般而言,新一层嵌套就缩进一点,如果需要换行,同一层的代码排成一行。因此,一个函数调用需要拆散成多行,看起来像下面这样:

 

(some-function arg-with-a-long-name

              another-arg-with-an-even-longer-name)

 

宏和其他特殊的form控制结构的缩进方式有点不同:“代码体”相对开括号有2个空格的缩进。

 

(defun print-list (list)

 (dolist (i list)

   (format t "item: ~a~%" i)))

 

SLIME会很好的帮你处理这些事情。用SLIME在每行的开头按tab键会适当的缩进,或者可以把光标定位到开括号再键入入C-M-q就对表达式格式化。或者在函数form中任意位置可以键入C-c M-q对函数form格式化。

 

一个重要的格式化规则是闭括号都是放在同一行,list的最后一个元素是闭合的。所以不会像下面这样写:

 

(defun foo ()

 (dotimes (i 10)

   (format t "~d. hello~%" i)

  )

)

 

而是像这样写:

 

(defun foo ()

 (dotimes (i 10)

    (format t "~d. hello~%" i)))

 

结尾的)))可能会不舒服,但是习惯括号缩进以后就会习惯了——不需要刻意把他们分到多行中。

 

最后,注释以14个分号开始,用法如下所示:

 

;;;; Four semicolons are used for a fileheader comment.

;;; A comment with three semicolons willusually be a paragraph

;;; comment that applies to a large sectionof code that follows,

(defun foo (x)

 (dotimes (i x)

   ;; Two semicolons indicate this comment applies to the code

   ;; that follows. Note that this comment is indented the same

   ;; as the code that follows.

   (some-function-call)

   (another i)              ; thiscomment applies to this line only

   (and-another)            ; andthis is for this line

   (baz)))

 

随后是构建Lisp程序的重要部分,函数,变量,宏。下一个是:函数function

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值