美文网首页
0基础——lisp学习笔记(一)

0基础——lisp学习笔记(一)

作者: ayusong870 | 来源:发表于2020-05-27 21:14 被阅读0次

    目录:

    1. Hello,world
    2. A Simple Database
    3. 语法和语义(待补充)
    4. 函数(Functions)
    5. 变量
    6. 序列变量的基本操作
    7. 标准宏
    8. 自定义宏(Macors)
    9. 数字、字符和字符串
      参考文献

    本文以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. 完整代码

    下面是第二章所开发简易数据库的完整代码:



    相关文章

      网友评论

          本文标题:0基础——lisp学习笔记(一)

          本文链接:https://www.haomeiwen.com/subject/rwwkkhtx.html