2011年6月30日星期四

面向Scheme用户的Common Lisp语言的小贴士【程序语言介绍】

* Scheme
Scheme的主要参考资源是SICP和R5RS,优点是语言设计的相当精巧,便于学习和理解程序的原理。

* Common Lisp
Common Lisp是Lisp最主要的方言之一,是为了标准化众多分支而产生的,在AI领域特别是符号计算方面有所应用。和Lisp的另一方言Scheme相比,更适合写实际可用的程序,而不限于演示的目的。
本文假定读者已经了解了Scheme语言的基本使用,将侧重于CL语言自身中有差异的部分,以减少在使用CL因为困惑而不自在。
Lisp的共同点是在于List,这里尽量把Scm和CL当作不同的语言来看待,避开去具体比较之间的相似和相异之处。
这里假设已经了解了Lisp系语言的基本使用和编程范式,将仅从语言特性角度来看反而能都看到CL的一些特别之处。

* 参考文档
** Common Lisp the Language, 2nd Edition
语言描述文档,对语言特性和提供的库都有详细的说明
** Common Lisp HyperSpec
参考手册,用于查阅,形式类似于按照关键词组织的卡片表格样式
** ANSI Common Lisp
偏向于语言特性的教学,很适合本文的话题,所以这里将按照这本书的结构来写

* 正文
** 缩进对阅读代码来说,相当重要。如下内容如有歧义请以文档为准
** 列表list是Lisp中重要的数据结构,同时Lisp语言的表达式也以list形式书写,表达式可以求值
** 断言predicate命名以p结尾
** t表示true而nil表示false,t是所有类型的supertype,nil是所有类型的subtype
** 为方便表示不同的操作符,下面将Function称为函数,将Special Form称为语句,将Macro称为宏,有时候在用法上之间的差异会模糊
** 用defun定义全局函数,用defparameter定义全局变量并计算初始值(如果仅仅是定义的话,也可用defvar),用defconstant定义全局常量
** 用let/let*绑定局部变量,用flet/labels绑定局部函数
** 宏destructuring-bind可以接受模式pattern形式的绑定
** 语句setf用来改变全局或局部符号或表达式所表示的空间所绑定的值,当符号未绑定时自动绑定全局符号
** 语句setf可以同时接受多组符号和值的绑定,这里的符号可以是符号类型,也可是表达式来指定符号的空间或者表示对对象的修改
** 函数参数从左到右执行,函数定义时允许递归调用
** 格式化函数format的第一个参数表示输出的方式,格式字符串~A表示在该处输出值,~%表示换行
** 格式化函数中的格式字符串有一些复杂但有效的用法
** 语句quote可以在包内获取或创建符号,或者用来创建列表,可缩写为'
** 语句function可以将符号或lambda表达式对应的函数,可缩写为#'
** 以lambda开头的不是函数或者语句,而是一种单独形式表达式,类似于列表
** 函数apply和funcall用来将函数作用到参数上,第一个参数可以是函数也可以是符号,但不建议是lambda表达式
** 判断值的类型可以用类型相关的断言,或者用函数typep进行比较,typecase进行分支,deftype定义类型,coerce进行类型转换
** 判断值的内容是否相同用eql,判断列表及每个元素相同用equal,判断值所在的地址是否相同用eq
** 列表list通过函数list生成或者通过make-list来创建,内部由点对pair构成
** 用copy-list和copy-alist和copy-tree来复制列表和关联列表和嵌套列表
** 函数mapcar用来通过某函数将列表的每个元素映射并新创建一个列表
** 列表可以充当集合Set使用,或者是序列Sequence,或者是堆Stack,或者是关联列表Assoc-list,在CL中提供有相关的函数
** 多维数组由函数make-array创建或者用#na生成,由函数aref引用下标,并可由setf来改变数组内的值
** 向量是一种一维数组,用vector或者#()来生成,通过svref来引用。数组的函数如sort和aref同样可用
** 字符串也是一种一维数组,可以用equal判断相等(区分大小写),同时也有专门用于字符串的函数如函数char来获取下标对应的字符以及不区分大小写的比较(以“string-”为前缀)
** 列表可用nth获取下标,不过由于是递归调用cdr/rest所以性能不佳。和向量及字符串一起,都可以使用序列相关的函数,如elt获取下标对应的元素
** 序列相关的函数常见的关键词是:key :test :from-end :start :end
** 形如(defstruct point x y)定义了两个成员的结构Structure,由(make-point :x 0 :y 0))创建实例,并得到访问函数point-x和point-y,提供有setf形式用于修改,断言名为point-p。
** 字段可有初始值,结构也由:cone-name定义对应函数的前缀,有:print-function定义打印方式
** 哈希表Hash Table由make-hash-table创建(其:test默认为eql),gethash访问(其第二个返回值表示是否存在)
** 控制流程中语句progn依次执行语句并返回最后一个值,语句block的第一个参数表示标签并可用return-from返回语句块的值(标签为nil时可用return,也可在一些语句内使用,函数名可作为标签使用),tagbody可在内部任意位置位置使用并由通过go来跳转
** 分支cond和case的最后一个分支可以是t或者otherwise表示默认的情况
** 循环和迭代过程可以使用do或do*或dolist或dotimes或mapc(不返回值的mapcar)或loop
** 语句可以返回列表也可以由values返回多个值,通常只有第一个值其作用,可由multiple-value-bind或multiple-value-call或multiple-value-list使用多返回值
** 非局部跳转(跨越函数调用)可以用throw(参数为标签加返回值)和catch和unwind-protect ,而错误error会触发error handler
** 函数symbol-function用来获取符号对应的全局函数,也可以用setf的形式使用,它不可以用来访问局部函数
** 用documentation可以获得defun中参数后的描述用字符串,函数之外的类型也可以定义这样的文档
** 参数列表Parameter List指函数定义和调用时的&rest &optional &key,可以有默认值
** 函数定义中可以指定某变量使用动态作用域,形如(declare (special x)),可以在修改全局变量时以免造成副作用的扩散
** 函数compile用来编译符号所对应的函数,有些操作符会在编译期执行的,会影响到性能和一些值的绑定。
** 递归可以用数学归纳法来考虑
** 标准输入输出流Stream所存放的全局变量是*standard-input*和*standard-output*
** 由make-pathname创建路径,函数open(:direction表示读写方式)由路径打开流,函数close关闭流
** 宏with-open-file所定义的语句有一个隐含的unwind-protect的close,其第一个参数的首元素是该语句中绑定到流的变量名
** 函数read-*默认使用以*standard-input*作为字符输入,函数read-from-string从参数的字符串作为输入流,在读取时read-macro会被展开
** 函数prin1可以和read配对使用(会将转义字符显示出来),通常的输出使用princ,函数pprint对应*print-pretty*非nil
** 函数format中的格式化字符串在~后可跟,分割的参数,参数可空,右侧,可省略
** 符号Symbol不区分大小写(对于包含空格等字符的符号名,可以用双"|"包围),会intern到包package中,不同的包里的同名符号是不同的。
** 以冒号为前缀的关键词keyword的值是其本身,与包的作用域无关。有时需要用gensym得到一个唯一的符号。
** 函数intern用于从字符串在包内查找或创建符号,每个符号有它的属性列表
** 包由宏defpackage创建,可以指定:use :nicknames :export,使用in-package切换到某包内
** 通过“包名:符号名”的形式来使用别的包里导出export的符号
** 当符号有绑定全局变量时,可以用symbol-value获得该变量的值(不同的类型可以绑定到同一个符号)。局部变量只在编译或解释存在,不可以混用
** 类型number分为integer float ratio complex,存在自动和手动的类型转换
** 用=可以比较两个数是否相等(“/=表示参数各不相等”),而eql不仅要相等好需要类型一致
** 整数分为fixnum(最大正值为常量most-positive-fixnum)和bignum,会自动转换。浮点数有四种,之间储存和计算的精度不同
** eval以列表作为参数,执行使用全局作用域
** 定义宏defmacro返回一个列表,语法类似于defun,用macroexpand-1可以手动展开,常配合Backquote书写
** 宏将在在使用它的程序执行前替换原表达式,需要区分编译时和运行时执行的部分,以及和调用的上下文的影响(建议配合let使用)
** 用于setf的同名宏通过define-modify-macro定义,setf中的表达式参数作为该宏的第一个参数
** 定义类defclass,其参数为类名,超类列表,槽列表。函数make-instance通过符号创建类,函数slot-value访问实例的槽
** 槽slot可以定义:accessor :initarg :initform,定义“:allcation :class”则槽属于类而非实例,槽还可以定义:documentation :type
** 子类会继承超类superclass的槽,类可以有多个超类,按照从到右深度优先
** 同名的槽,:allocation :initform :documentation取做特殊化的类(子类),:initargs :accessors :readers :writers取并集,:type取交集,同名依据符号相同来判断
** standard-object是standard-class的实例,是t的subtype,是其他所有class的superclass
** 宏defmethod用于定义方法,可以给参数定义class或者type或者eql条件
** 方法必须有相同数量的参数,可选参数也需要个数一致,&rest或&key需要同时存在或不存在。其中只有必须的参数可以特例化,如果类型一致会覆盖先前定义的特例方法
** 由方法定义的函数称为generic function,可以对不同的类定义不同的方法,类型依照参数从左到右的顺序匹配定义了特例且最特例的类型,这有消息转递模型所差异
** 可以在方法的基础上定义辅助auxiliary方法:before(按照特殊往一般的顺序) :after(按照一般往特殊的顺序) :around(只执行最特殊的,可以(if (next-method-p) (call-next-method)))
** 宏defgeneric用于将所有的方法看作一个整体,例如可用:method-combination通过某操作符混合所有的方法
** CLOS可以配合PACKAGE机制来只导出需要暴露的函数
** 列表类型(也包括其他的容器)中会存在节点数据的共享,并可能构成环(打印需要*print-circle*指定为t,使用#n=和#n#来读入)。特别要注意副作用会影响到所有引用它的数据,例如member就会引用原数据,而list会创建一个新的列表(quote就不会)
** 操作列表的参数以形式(setf list1 (** list1))来使用,这里的操作符有的会创建新的列表,有的则是破坏性的destructive
** 破坏性的操作会改变对象的数据,但是不保证会做出如何的修改,而是确保返回值是正确的,这样可以比完全重新创建列表有性能上的好处
** 有无破坏性的函数可能会对应提供,如mapcar和mapcan,remove和delete,append和nconc,subst和nsubst,通常以命名以n为前缀(表示non-consing)
** 可以用declare定义函数的编译参数,以及用declaim定义全局的,包括optimize inline type,用the定义表达式的类型,数组时可定义:element-type,已知类型可以加快对方法的查找。性能的因素可以在不影响语句时再完善
** 可以定义类型,如(vector fixnum 20),(simple-array fixnum (4 4)),(integer 1 100),(simple-array fixnum (* *)),(or vector (and list (not (satisfies circular?))))
** 容易可以定义大于:fill-pointer的空间,以便添加元素用。创建pool可以重复使用以创建的对象,减少cons和gc的开销
** 当流是:element-type 'unsigned-byte时可以read-byte/write-byte
** 定义read-macro通过set-macro-character以及set-dispatch-macro-character来通过流返回列表
** 用户默认包是common-lisp-user,可以用make-package创建用in-package进入,用export导出use-package使用
** 宏loop用来书写迭代过程,可以仿照已有的例子来使用
** 函数error ecase check-type assert会抛出错误,可用ignore-errors转化为nil
** 语句or和and是语句,本身不是函数,也不像C++中那样可以overload为函数
** 可以使用trace以及交互模式中的指令对代码进行调试,交互回话可以保存为映像
** 通过函数load载入文件,通过eval-when在设置在装载或编译时执行语句
** 可以根据所使用的类型,有必要知道一下CL已经自带的一些函数或宏或方法

* Wiki
http://www.cliki.net

* 实现
我暂且用的是gcl和clisp,因为比较容易上手,话说自己还没什么CL的使用使用经验。
IDE可以用SLIME,LispIDE或者带tag和高亮和括号匹配的文本编辑器也行,Cusp不知还在维护否。
有一些开源的符号计算软件使用CL实现的,可以作为学习的资源和参考。

* 其他方言或相关语言
** Emacs Lisp
** Visual Lisp
** Clojure
** Dylan

* 接下来...
** 面向Scheme用户的ML语言的小贴士
** 面向Scheme用户的Haskell语言的小贴士

----
用org-mode列了一个提纲,展开缺乏,看着有点别扭。
有些细节可能会有理解误差,这里仅仅是导读的目的了。
此文纯介绍性质,不建议随意地在实际目的中使用。

----
关于另一个很重要的特性,就是程序边运行边修改,自己还没有这方面的经验和习惯,但是CL的一些语言设计确实有为这种用法考虑。
如果是有经验实现可以选择SBCL+SLIME,本文只是一个浅尝辄止的态度的产物,虽然出发点是想说说Scm和CL的差异的说。
恩...有些地方简略地有些不明不白了...

----
貌似说reduce要优于不定长参数。
loop宏和尾递归是两种风格不同的东西,前者算是更进一步的抽象吧。

1 条评论:

zaxonzaccaria 说...

Top 10 games for Android - DrmCD
The Best Android Slots The first, well-known, original free 삼척 출장마사지 casino 동해 출장샵 slot game developed by 경기도 출장샵 NetEnt was created in 1993 and 강릉 출장안마 developed in 군포 출장마사지 1998.