目录:
- Hello,world
- A Simple Database
- 语法和语义(待补充)
- 函数(Functions)
- 变量
- 序列变量的基本操作
- 标准宏
- 自定义宏(Macors)
- 数字、字符和字符串
参考文献
本文以Practical Common LISP一书的学习笔记开始,逐渐深入讨论LISP语言
1. Hello, world
Common Lisp是lisp一个比较流行的方言,Common Lisp的官网在https://common-lisp.net/,官方推荐的入门版本又分为Steel Bank Common Lisp (SBCL) 和Clozure Common Lisp (CCL) ,这两个版本入门比较简单(wiki中有详细的方言列表:http://en.wikipedia.org/wiki/Common_Lisp#List_of_implementations)。
官网也提供了比较多的入门资料一些在线资料:
Practical Common Lisp(http://www.gigamonkeys.com/book/)
Lisp in Small Parts(http://lisp.plasticki.com/show?14F)
Common Lisp: A Gentle Introduction to Symbolic Computation(http://www-cgi.cs.cmu.edu/afs/cs.cmu.edu/user/dst/www/LispBook/index.html)
Successful Lisp: How to Understand and Use Common Lisp(http://www.psg.com/~dlamkins/sl/cover.html)。
本人选择CCL作为入门语言,在官网下载CCL编译器。
Lisp并不支持一般的表达式,如果想得到1+2的结果需要输入
(+ 1 2)才能得到3。下面的语句定义了hello world函数:
?(defun hello-world () (format t "hello,world"))
HELLO-WORLD
defun:定义函数;
Hello-world:函数名称;
():函数参数列表;
(format t “hello,world”):格式化字符串,声明为t。
调用hello-world函数要依照规则
? (hello-world)
hello,world
NIL
Lisp程序的默认扩展名为.lisp,假设有一个lisp程序文件名为hello.lisp,那么在lisp REPL中可以通过 (load "hello.lisp")调用,而
(load (compile-file "a.lisp"))可以预编译文件。
2. A Simple Database
2.1. LIST function
Lisp中最基本的结构就是列表,使用LIST函数定义列表方式如下:
? (list 1 2 3)
(1 2 3)
也可以为列表提供关键字,这样的列表称之为property list(属性列表,简称plist):
? (list :a 1 :b 2 :c 3)
(:A 1 :B 2 :C 3)
可以用getf函数获取plist中关键字对应的值:
? (getf (list :a 1 :b 2 :c 3) :a)
1
? (getf (list :a 1 :b 2 :c 3) :c)
3
现在自定义函数make-cd用来生成cd数据信息:
? (defun make-cd (title artist rating ripped)
(list :title title :artist artist :rating rating :ripped ripped))
MAKE-CD
调用make-cd生成cd数据信息:
? (make-cd "Roses" "Kathy Mattea" 7 t)
(:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T)
2.2. 存储数据
一条单一的数据(还是未命名的),没有任何实际意义,因此需要一个更大的数据结构来存储数据。DEFVAR宏可以用来定义全局变量,默认值为nil(空)。
? (defvar *db* nil)
*DB*
PUSH宏将新建的数据推到全局变量db中:
? (push (make-cd "Roses" "Kathy Mattea" 7 t) *db*)
((:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
自定义函数add-record,减轻输入负担:
? (defun add-record (cd) (push cd *db*))
ADD-RECORD
? (add-record (make-cd "Fly" "Dixie Chicks" 8 t))
((:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)
(:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
? (add-record (make-cd "Home" "Dixie Chicks" 9 t))
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
(:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)
(:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
直接输入变量的名称,可以在命令行中显示变量内容:
? *db*
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T)
(:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T)
(:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
2.3. 格式化输出,神奇的format表达式
如果为了观察db的内容,直接输入db返回的结果太乱了,如果是下面这样就好了:
TITLE: Home
ARTIST: Dixie Chicks
RATING: 9
RIPPED: T
TITLE: Fly
ARTIST: Dixie Chicks
RATING: 8
RIPPED: T
TITLE: Roses
ARTIST: Kathy Mattea
RATING: 7
RIPPED: T
接下来,请不要震惊,看神代码:
? (format t "~{~a:~10t~a~%~}~%" (make-cd "Rose" "Kathy Mattea" 7 t))
TITLE: Rose
ARTIST: Kathy Mattea
RATING: 7
RIPPED: T
NIL
还是逐渐来分析这些奇怪的指令吧,首先所有的格式化指令都已开头(类似C语言中的%),a相当于将参数输出为人类可读的格式。在C语言中格式化输出变量,int对应%d,float对应%f,string对应%s,由于lisp没有具体的变量类型~a就好理解了。a是aesthetic的缩写。
? (format t "~a" "Dixie Chicks")
Dixie Chicks
NIL
? (format t "~a" :title)
TITLE
NIL
? (format t "~a:" "Title")
Title:
NIL
? (format t "~a:~10t~a" :artist "Dixie Chicks")
ARTIST: Dixie Chicks
NIL
? (format t "~a+~a=~a" 1 2 (+ 1 2))
1+2=3
NIL
~10t就表示前一个输出的首字母(包括)距离后一个输出的首字母10个字符距离。
ARTIST: Dixie Chicks
123456789a
~%表示换行:
? (format t "~a~%~a~%~a" 1 2 3)
1
2
3
NIL
{与}表示循环遍历list中的每一个元素,已{与}包含的格式输出:
? (format t "~{number:~a~%~}" (list 1 2 3 4 5 6 7 8 9 10))
number:1
number:2
number:3
number:4
number:5
number:6
number:7
number:8
number:9
number:10
NIL
现在回过头来看下面代码,是不是就可以理解了呢:
(format t "~{~a:~10t~a~%~}~%" (make-cd "Rose" "Kathy Mattea" 7 t))
自定义函数dump-db来格式化输出db中的数据,下面代码中 唯一陌生的宏dolist用来遍历db中的每一个元素,并命名为cd。
? (defun dump-db ()
(dolist (cd *db*) (format t "~{~a:~10t~a~%~}~%" cd)))
DUMP-DB
? (dump-db)
TITLE: Home
ARTIST: Dixie Chicks
RATING: 9
RIPPED: T
TITLE: Fly
ARTIST: Dixie Chicks
RATING: 8
RIPPED: T
TITLE: Roses
ARTIST: Kathy Mattea
RATING: 7
RIPPED: T
NIL
再思考一下,其实利用{与}可以省略dolist:
? (defun dump-db () (format t "~{~{~a:~10t~a~%~}~%~}" *db*))
DUMP-DB
? (dump-db)
TITLE: Home
ARTIST: Dixie Chicks
RATING: 9
RIPPED: T
TITLE: Fly
ARTIST: Dixie Chicks
RATING: 8
RIPPED: T
TITLE: Roses
ARTIST: Kathy Mattea
RATING: 7
RIPPED: T
NIL
2.4. 用户输入
开发软件主要就是解决数据结构、用户交互、数据存储等,数据结构已经解决了,用户交互的输出显示也解决了,那么这一节用来解决用户输入。
首先介绍两个函数force-output和read-line。force-output用来等待缓存的输出,在输出完成前不做任何处理(个人感觉,现在计算机的运行速度force-output的作用不明显,但还是有必要的)。read-line用来读取当前光标位置至本行结尾(不包括换行符)。
现在用format、force-out、read-line完成自定义函数prompt-read,用来处理CD的某一个属性的输入。
?(defvar *query-io*)
?(defun prompt-read (prompt)
(format *query-io* "~a: " prompt)
(force-output *query-io*)
(read-line *query-io*))
PROMPT-READ
需要注意通过上面自定义函数的输入都是字符串:
? (prompt-read "Title")
Title: blackjack
"blackjack";带””,是字符串
NIL
在数据输入时,字符串都好说,但数字就需要转化了。parse-integer用来将字符串转化为数字,但如果字符串无法转化为数字将报错终止程序(并且字符串得是完全的数字组成)。为parse-integer添加junk-allowed参数将允许字符串中包含非数字,数字开头的字符串将成功转化,非数字开头的字符串将返回NIL。
? (parse-integer "a10" :junk-allowed t)
NIL
0
? (parse-integer "10a" :junk-allowed t)
10
2
如果返回了NIL,我们默认将其看作是0,那么就需要借助OR宏,OR可以有多个参数,lisp将逐一计算每一个参数,返回第一个非NIL的结果。
? (or NIL NIL 1 2)
1
? (or (parse-integer "a10" :junk-allowed t) 0)
0
? (or (parse-integer "10a" :junk-allowed t) 0)
10
函数Y-OR-N-P用来强制用户输入y或Y或n或N,如果不是将不断循环,输入y或Y返回T,输入n或N返回NIL。另外y-or-n-p可以代一个参数,不带参数使提示(y or n),代参数时另外提示参数内容。
? (y-or-n-p)
(y or n) k
Please answer y or n. (y or n) n
NIL
? (y-or-n-p)
(y or n) y
T
? (y-or-n-p "Hi,")
Hi, (y or n) y
T
下面可以定义prompt-for-cd函数用来处理cd的属性输入了。
? (defun prompt-for-cd ()
(make-cd
(prompt-read "Title")
(prompt-read "Artist")
(or (parse-integer (prompt-read "Rating") :junk-allowed t) 0)
(y-or-n-p "Ripped [y/n]: ")))
PROMPT-FOR-CD
借助y-or-n-p函数可以用来实现不断的录入cd内容,直至用户不想录入。
? (defun add-cds ()
(loop (add-record (prompt-for-cd))
(if (not (y-or-n-p "Another? [y/n]: ")) (return))))
ADD-CDS
? (add-cds)
Title: blackjack
Artist: song
Rating: 2
Ripped [y/n]: (y or n) y
Another? [y/n]: (y or n) y
Title: stack
Artist: song
Rating: 3
Ripped [y/n]: (y or n) y
Another? [y/n]: (y or n) n
NIL
现在来查看一下数据库(内存)中的内容。
? (dump-db)
TITLE: stack
ARTIST: song
RATING: 3
RIPPED: T
TITLE: blackjack
ARTIST: song
RATING: 2
RIPPED: T
TITLE: Home
ARTIST: Dixie Chicks
RATING: 9
RIPPED: T
TITLE: Fly
ARTIST: Dixie Chicks
RATING: 8
RIPPED: T
TITLE: Roses
ARTIST: Kathy Mattea
RATING: 7
RIPPED: T
NIL
2.5. 数据的存储
宏WITH-OPEN-FILE用来绑定文件流到一个变量,执行一些操作,然后关闭文件流,其中:
:direction :output;声明打开文件为了写
:if-exists :supersede;如果文件存在则重写
with-standard-io-syntax;表示标准的IO异常处理
自定义函数来保存数据:
(defun save-db (filename)
(with-open-file (out filename
:direction :output
:if-exists :supersede)
(with-standard-io-syntax
(print *db* out))));print宏用来将变量输出到流中
自定义函数来读取数据:
? (defun load-db (filename)
(with-open-file (in filename)
(with-standard-io-syntax
(setf *db* (read in)))))
LOAD-DB
setf宏用来给变量赋值,with-open-file的默认参数是:direction :input,因此无需特别声明。read行数用来读取流中的数据。
1.2. 查询数据库
实现查询数据库前,先介绍下REMOVE-IF-NOT函数,用来在list中排除不是判断条件的元素,返回满足判断条件的元素组成新的list。
如果没有满足判断条件的则返回boolean值NIL。下面的例子从数列中排除不是偶数的数字。
? (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10))
(2 4 6 8 10)
符号#’将后面的名称当做函数对待,否则将被当做变量。#’的意思就是“找到下面名称的函数”。(remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10))相当于对列表中的每一个元素执行函数evenp,返回结果为NIL移除。
evenp函数相当于(lambda (x) (= 0 (mod x 2))),下面的返回是一样的,列表中的每一个元素被当做lambda表达式中的输入参数x,执行(= 0 (mod x 2))的运算,x模2等于0则返回ture否则返回NIL。lambda并非函数名也非宏名称,只是表示下面的内容被声明为匿名函数。
? (remove-if-not #'(lambda (x) (= 0 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10))
(2 4 6 8 10)
现在回顾2.1节中介绍的getf方法,若要搜索:artist值为"Dixie Chicks"的cd,首先获取所有:artist值,(getf cd :artist)在判断是否等于"Dixie Chicks",(equal (getf cd :artist) "Dixie Chicks"),在利用remove-if-not筛选。
? (remove-if-not
#'(lambda (cd) (equal (getf cd :artist) "Dixie Chicks")) *db*)
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))
整理为自定义函数:
(defun select-by-artist (artist)
(remove-if-not
#'(lambda (cd) (equal (getf cd :artist) artist))
*db*))
PS:下面讨论一个并不美丽的实现方式,但是也表达了lisp的一种函数调用方式。利用匿名函数的特点,匿名函数定义是与defun不同,defun在程序执行段的内存中专门开辟出一段地址记录这个函数,在函数调用时程序指令指针会跳转到函数地址的位置执行,执行完毕再回到主程序的下一句指令。而匿名函数是在编译时将匿名函数的内容拷贝到当前的代码段。
因此利用这种特性,可以先将搜索器抽象出来,定义为自定义函数,再在select函数中调用选择器。程序编译时相当于将选择器中的代码拷贝到算则函数中:
?(defun artist-selector (artist)
#'(lambda (cd) (equal (getf cd :artist) artist)))
;再自定义选择函数:
?(defun select (selector)
(remove-if-not selector *db*))
?(select (artist-selector "Dixie Chicks"))
((:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T))
只是这样我们需要实现每一个selector,artist-selector、title-selector、rating-selector、ripped-selector。作为一个懒人,这样烦死了。
再次回顾(getf (list :a 1 :b 2) :a)这个例子,其中选择关键字:a可否用变量替代呢。
? (defvar key :a)
KEY
? (getf (list :a 1 :b 2) key)
1
这样可以将select-by-artist函数改造成利用这种特性,这样就不用实现每一种属性的索引了:
(defun select (key value)
(remove-if-not
#'(lambda (cd) (equal (getf cd key) value))
*db*))
2.7 多重查询
前面已经讨论了基本的查询功能,但是只能查询一个条件。我们往往想一次查询多个条件已更精准的查找(例如select :title “Home” :artist “song”)。那么此时函数的输入参数数量是不定的,像lisp中,(+ 1 2 ..)、(and t nil t..)等等都可以输入多个参数,要实现这样的功能,需要在定义函数时运用lisp的&rest 参数功能,例如下面的函数计算了所有输入参数的和:
(defun mysum (&rest numbers)
(loop while numbers
summing(pop numbers)))
? (mysum 0 1 2 3)
6
下面介绍另一个比较炫的功能,“”符号。首先输入(and 0 1)看看输出是什么?再输入一下
(and 0 1),将`的语句原封不动的输出了,这是什么意思呢?。
? (and 1 0)
0
? `(and 1 0)
(AND 1 0)
其实这是macro的一种高级应用,下面定义一个宏,看看结果。
? (defmacro mymacro ()
`(and 1 0))
MYMACRO
? (mymacro)
0
虽然这样定义的宏运行结果与没有`符号相同,但是是有本质区别的。
? (defmacro mymacro ()
(and 1 0))
MYMACRO
? (mymacro)
0
我们可以利用这种特性,动态生成要执行的语句,然后在宏中执行。下面介绍下,@符号,在`中,@符号的意思是分割表达式中的值。
? `(and ,(list 1 2 3)
)
(AND (1 2 3))
? `(and ,@(list 1 2 3))
(AND 1 2 3)
那么现在来写一段没有任何意义仅仅为了表示用法的代码,可以看到只有在宏中才会执行的到结果:
? (defmacro myand (&rest a)
`(and ,@a))
MYAND
? (myand t nil t)
NIL
? (defun myand2 (&rest a)
`(and ,@a))
MYAND2
? (myand2 t nil t)
(AND T NIL T)
利用上面介绍的内容就可以逐渐的实现多重查询了,首先自定义函数创造表达式:
(defun make-expr (key value)
`(equal (getf cd ,key) ,value))
? (make-expr :artist "song")
(EQUAL (GETF CD :ARTIST) "song")
处理多个输入创建表达式列表:
(defun make-expr-list (fields)
(loop while fields
collecting (make-expr (pop fields) (pop fields))))
? (make-expr-list (list :artist "song" :title "home"))
((EQUAL (GETF CD :ARTIST) "song") (EQUAL (GETF CD :TITLE) "home"))
执行拼凑好的语句的select宏:
(defmacro select (&rest selector)
`(remove-if-not #'(lambda (cd) (and ,@(make-expr-list selector))) *db*))
? (select :artist "song" :title "Home")
((:TITLE "Home" :ARTIST "song" :RATING 2 :RIPPED NIL))
2.8. 更新数据
select宏将满足查询条件的结果返回成新的list,但是可以对新的list进行操作,而影响原始list。这样我们可以像
(update (select :artist “song”) :rating 10) 这样更新数据库。
自定义函数update,使用临时变量保存select结果,然后用dolist遍历并改变对应属性的值。
(defun update (selector-fn key value)
(defvar tmp nil)
(setf tmp selector-fn)
(dolist (cd tmp)
(setf (getf cd key) value))
(print-db tmp))
? (update (select :artist "song") :rating 10)
TITLE: Home
ARTIST: song
RATING: 10
RIPPED: NIL
TITLE: blackjack
ARTIST: song
RATING: 10
RIPPED: T
NIL
但是上面的函数仅能修改一个属性的值,我们还是希望能够修改多个属性,那么添加一个函数,专门处理多属性值的修改。
(defun make-updates (cd newKV)
(loop while newKV
do (setf (getf cd (pop newKV)) (pop newKV))))
在update函数中调用make-update即可。
(defun update (selector-fn &rest newKV)
(defvar tmp nil)
(setf tmp selector-fn)
(dolist (cd tmp)
(make-updates cd newKV))
(print-db tmp))
? (update (select :artist "song") :rating 18 :ripped nil)
TITLE: Home
ARTIST: song
RATING: 18
RIPPED: NIL
TITLE: blackjack
ARTIST: song
RATING: 18
RIPPED: NIL
NIL
写出这种update函数的实现后,再回想select宏的实现,其实也可能按照常规的编程思路去写,只不过lisp的思路写代码更简洁更高效。就是太难理解了,仅从入门样例中还无法学到精髓。这里的update实现先这样,等逐渐的深入了解lisp后,再进行改造,更多的运用lisp特性。
2.9. 删除数据
与remove-if-not对应的一个函数是remove-if,这将返回与判断条件相违的结果,那么利用remove-if来实现删除数据再好不过了,实现与select基本一致。
(defmacro delect (&rest selector)
`(setf *db* (remove-if #'(lambda (cd) (and ,@(make-expr-list selector))) *db*)))
? (delect :title "Home" :artist "song")
((:TITLE "blackjack" :ARTIST "song" :RATING 2 :RIPPED T) (:TITLE "Home" :ARTIST "Dixie Chicks" :RATING 9 :RIPPED T) (:TITLE "Fly" :ARTIST "Dixie Chicks" :RATING 8 :RIPPED T) (:TITLE "Roses" :ARTIST "Kathy Mattea" :RATING 7 :RIPPED T))
2.10. 完整代码
下面是第二章所开发简易数据库的完整代码:
网友评论