clisp 学习笔记 1-简述

现在看来,学习 Lisp 更多是为了学习而非工程实践,因此当时我实在不应该选择 Common Lisp,它工程味道太浓了。将来再要去接触 Lisp,首选 Racket,而若是为了工程实践,则 Clojure。

——2022.01.02

…… 我决定去学 Haskell 了,再见,Racket!

…… 我决定去学 Racket 了,再见,Clojure!

…… 我决定去学 Clojure 了,再见,common lisp!

你好,common lisp,我又决定先来找你玩了!

我又决定去学 Haskell 和 Clojure 了,再见,common Lisp!

哈哈哈哈哈哈哈

QA

Q:为什么学 lisp?
A:它酷!

Q:酷有个屁用?
A:****!

Q:根据哪里学的?
A:ANSI Common Lisp

lisp 语言的形式

1
(format t "hello!world!") ; 我是注释

lisp 语言只有一种形式(数据和代码都采用这种格式)(不算注释的话)——上述的前序表达式。

lisp 语言必须使用括号包裹。否则在某些时候无法判断表达式什么时候结束,比如——

1
2
3
(+ 2 3)
(+ 2 3 4 5)
(/ (- 7 1) (- 6 4))

求值

lisp 是这样求值的——

  1. 首先从左至右对实参求值。
  2. 实参的值传入以操作符命名的函数

在这个例子中——

1
(+ 2 3 4 5)

lisp 对 2 求值,结果为 2,对 3 求值,结果为 3,对 4 求值,结果为 4,对 5 求值,结果为 5,然后将这些值传给+函数,返回 14。

数据

符号 symbol 和列表 lists

符号是单词(words),其不能(无法?)对自身求值,应当使用’引用。

由括号包裹的零个或多个元素称为列表。列表应该使用’来引用,因为其结构和函数调用的结构相同,lisp 会认为这是函数调用。

1
2
3
4
5
> 'Hello!world123 ;symbol
HELLO!WORLD123

> '(the list (a b c) has 3 elements) ;list
(THE LIST (A B C) HAS 3 ELEMENTS)

quote 函数(它不遵守 CLisp 既定的求职规则),它和引用起相同作用。

1
2
3
4
5
> '(+ 3 2)
(+ 3 2)

> (quote (+ 3 2))
(+ 3 2)

列表表示 lisp 程序

lisp 的程序是用列表来表示的,这是 lisp 最卓越的特性。lisp 的代码如果被引用,就对自身求值,如果没有,则被视为代码,按照求值规则求值后返回值。

1
2
> (list '(+ 2 1) (+ 2 1))
((+ 2 1) 3)

因此,lisp 代码是容易写出 lisp 代码的。并且 lisp 程序员应经常地使用此特性来解决问题,“让 lisp 适应问题,而非让问题适应 lisp”。

NIL

lisp 有两种方式表示空列表。

1
2
3
4
> ()
NIL
> nil ;nil 显然是一个求值为 NIL 的 symbol
NIL

列表操作

cons

cons 的第二个实参是列表(记住,列表是要引用的,否则它会被当成函数调用),它将第一个实参加入到第二个实参这个列表,得到一个新列表。

1
2
3
4
5
6
7
8
9
10
11
> (cons 'a '(b c d))
(A B C D)

> (cons 'a NIL)
(A)

> (cons '(a b c) '(b))
((A B C) B)

> (cons '(a b) 'c) ; 瞎搞呢?看上去它是对 c 这个符号进行了一些处理……
((A B) . C) ; 啥原因?

list

list 将所有实参加入到一个列表中。

1
2
3
4
5
> (list 'a 'b 'c)
(A B C)

> (list '(a b) '(c (d e)))
((A B) (C (D E)))

car & cdr

car 返回第一个元素,cdr 返回第一个元素后的所有元素(以列表的形式)。

1
2
3
4
> (car '(a b c))
A
> (cdr '(a b c))
(B C)

混合使用 car 和 cdr 可以取到列表中的任何元素。

真假

谓词(predicate)

谓词是 lisp 中返回真假的函数,就像其他语言的返回布尔值的函数。一般而言,其结尾是 p。

1
2
> (listp '(a b c))
T

not & null

t 表示逻辑真,t 也是对自身求值的,与 nil 相同。

逻辑假用 nil 表示。

1
2
> (listp 26)
NIL

not 和 null 在逻辑上是不同的,前者判断是否是逻辑假,后者判断是否是空表,可是实际上其结果是相同的。

1
2
3
4
5
> (null nil) ; 空表
T

> (not nil) ; 逻辑假
T

条件表达式 if

if 接受三个实参,一个 test 表达式,一个 then 表达式,一个 else 表达式。if 会对 test 表达式求值,如果为真,则求 then,否则求 else。

1
2
3
4
> (if (listp '(a b c))
(+ 1 2)
(+ 5 6))
3

if 也是特殊的操作符,如同 quote。if 不遵守求值规则之处在于,它对最后两个实参,只会求其中一个的值。因此,if 是不能用函数来实现的。

要注意的是,非 nil 的,都是 t

and & or

and 和 or 都从左往右求值。但是,and 在遇到 NIL 时停止求值,返回 NIL 或最后一个参数的值,如果其他参数全为真

or 在遇到参数为逻辑真时停止求值,返回这个值

1
2
3
4
5
6
7
8
9
10
11
12
13
> (and (+ 1 2) (+ 2 3))
5
> (and (+ 1 2) nil)
nil
> (and nil (format t "rua")) ; 遇到 nil 就停止求值了
nil

> (or (+ 1 2) nil)
3
> (or nil (+ 1 2))
3
>(or (+ 3 1) (format t "hello")) ; 遇到 T 就停止求值
4

and 和 or 这两个操作符称为,它们和特殊的操作符一样,可以绕过一般的求值规则。

函数

定义新函数的函数叫 defun(它显然也是不遵守求值规则的),它接受三个以上的实参——一个名字,一组用列表表示的实参(而这个实参中的内容将被作为函数的形参),以及一个或多个组成函数体的表达式。

1
2
3
4
5
6
7
8
9
10
> (defun say (obj) (format t obj)) ;obj 是形参
SAY

> (say "hello")
hello
NIL

> (SaY "hello")
hello
NIL

可见,在进行函数调用时,它会把函数名进行求值,转换成全大写的 symbol。

符号(symbol)就是变量(和函数)的名字,符号本身以对象存在。因此,符号和列表必须要以引用形式来使用,否则列表会被当成函数,符号会被当成变量。

递归

没啥可说的。

判断一个 tar 是否在一个列表中,如果是,返回这个 tar 及以后的子列表,否则返回 NIL。

1
2
3
4
5
6
7
8
9
10
11
(defun our-member (tar lst)
(if (null lst)
nil
(if (eql (car lst) tar)
lst
(our-member tar (cdr lst))))) ; 别看了,我们 lisp 就是这样的。

(defun fib (n)
(if (or (= n 1) (= n 2))
1
(+ (fib (- n 1)) (fib (- n 2))))) ; 最睿智的 fib 解决法

IO

输出

最普遍的 clisp 输出函数时 format,接受两个两个及以上的实参,第一个实参决定要打印到的位置,第二个实参是字符串模板,剩余的实参通常是要插入到字符串模板。

t 表示输出送到缺省的地方(通常是顶层,或者是控制台?),A 表示被填入的位置,%表示换行符。

1
2
3
> (format t "~A plus ~A equals ~A. ~%" 2 3 (+ 2 3))
2 plus 3 equals 5. ;format 的输出
NIL ;format 的返回值

输入

clisp 的标准的输入函数是 read,当没有实参时,会读取缺省的位置,通常是顶层。它会返回输入的东西被处理后的结果。

read 非常强大,虽然还不知道到底如何强大 w

1
2
3
4
5
6
7
> (defun askem (string)
(format t "~A" string)
(read))

> (askem "How old are you?")
How old are you?29
29

变量和赋值

let

let 引入局部变量,它只在 let 的括号内有效。
let 表达式有两个部分,第一个部分是创建新变量,其格式为 (variable expression)。每一格变量会被赋予相对应表达式的值。

1
2
3
> (let ((x 123) (y 456)) ; 每一个变量定义都是一个列表
(+ x y)))
579

写一个要求用户输入数字的函数,如果输入的是数字,则返回它,否则返回该函数。

之后,用>标识返回值。

1
2
3
4
5
6
7
8
9
10
11
12
(defun ask-number ()
(format t "enter a number:")
(let ((val (read))) ; 创建变量
(if (numberp val) ; 数字谓词
val
(ask-number))))

(ask-number)
> enter a number:oh
> enter a number:(aha)
> enter a number:23
> 23

defparameter & defconstant

1
2
3
4
5
6
7
8
(defparameter *glob* 99) ; 全局变量通过 defparameter 声明,通常使用星号包围以避免与局部变量混淆
> *GLOB*

(defconstant limit (+ *glob* 1)) ; 全局常量不需要用星号包围,因为如果有相同名字就会产生错误
> 100

> (boundp '*glob*) ; 全局量谓词,检测是否是全局变量或全局常量
T

setf

setf 是最普遍的赋值操作符。setf 的第一个实参可以为全局或局部变量,可以为符号,可以为表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(setf *glob* 98) ; 给全局变量* glob *赋值 98
> 98

(let ((n 10))
(setf n 2) ; 给局部变量 n 赋值 2
n)
> 2

(setf x '(a b c)) ; 隐式创建全局变量 x,这是不推荐使用的
> (A B C)

(setf (car x) 'n) ; 表达式也可以做第一个实参,此时更改这个实参引用的位置。
> N

x
> (N B C)

(setf
a 'b
c 'd
e 'f) ; 偶数个实参,可以定义多个变量。返回最后一个符号

任何引用到特定位置的表达式都可以做 setf 的第一个实参

函数式编程

函数式编程意味着只利用返回值,不造成副作用,函数式编程是 lisp 的主流范式,大部分 lisp 的内置函数都是函数式的

1
2
3
4
5
(set lst '(c a r a t))
> (C A R A T)

(remove 'a lst)
> (C R T) ; 这是一个新列表

副作用越少,技术越高 w,这意味着尽量少用 setf 这类的函数。

迭代

迭代总是比递归自然的……

do

do 宏是 clisp 的最基本的迭代操作符,和 let 类似,do 也可以创建局部变量。

第一个实参是一组变量的规格说明列表,每个元素是这样的形式 (varible initial update)。
第二个实参包含一个或多个表达式,第一个表达式测试迭代是否结束,最后一个表达式是 do 的返回值。
其余实参是 do 循环的函数体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defun show-squares (start end)
(do ((i start (+ i 1))) ((not (< i end)) 'done) ; 每次循环都测试条件,如果失败,则继续循环,直到成功,结束循环……如果能和 for 统一一下就好了……但是加上一个 not 就行……结束循环返回 done
(format t "~A ~A~%" i (* i i))))

(show-squares 2 5)
> 2 4
> 3 9
> 4 16
> 5 25
> DONE

(defun show-squares (i end) ; 递归版
(if (> i end)
'done
(progn ; 相当于是创建了一个函数体,依次执行每一个表达式,返回最后一个的值
(format t "~A ~A~%" i (* i i))
(show-squares (+ i 1) end))))

dolist

dolist 更为简单,它遍历一个列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(defun my-length (lst); 获取列表长度
(let ((len 0))
(dolist (obj lst) ; 对 lst 的每一个 obj,让 len+1
(setf len (+ len 1)))
len)) ; 别忘记返回值啊傻子

(defun my-length (lst); 递归版……这个彩虹括号似乎并没有什么用……
(if (null lst)
0
(+ (my-length (cdr lst)) 1))) ; 但是这个递归不是尾递归,效率不够高

(defun my-length (lst len) ; 尾递归版本,舒服!从 0 开始调用
(if (null lst)
len
(my-length (cdr lst) (+ len 1))))

函数作为对象

把函数当成变量一样的对象看待,是稀松平常的,js 喜欢这个。

1
2
3
4
5
(function +)
> #<SYSTEM-FUNCTION +>

#'+
> #<SYSTEM-FUNCTION +>

#’是 function 的缩写,就如同’是 quote 的缩写一样,

apply

函数可以作为实参传入。如 apply 函数。它接受任意数量的实参,但是要求第一个实参是函数,最后一个实参是列表

1
2
3
4
5
6
7
8
(apply #'+ '(1 2 3 4 1 2 3))
> 16

(+ 1 2 3 4 1 2 3)
> 16

(apply #'+ 1 2 3 '(4 1 2 3))
> 16

funcall

funcall 进行一样的工作,但是不需要把实参包装成列表。

1
2
(funcall #'+ 1 2 3)
> 6

lambda

lambda,匿名函数一样的东西。它似乎不能赋值给变量

1
2
3
4
5
6
7
8
9
10
11
((x) (+ x 100)) ; 这玩意运行不过去啊……

(lambda (x y)
(+ x y))
> #<FUNCTION :LAMBDA (X Y) (+ X Y)>

((lambda (x y)(+ x y)) 1 2)
> 3

(funcall #'(lambda (x) (+ x 5)) 10) ; 或者这样用
> 15

类型

lisp 的变量默认没有类型(当然,值有类型)。

lisp 的内置类型组成了一个类的层级,任何对象总是不止属于一个类型。
比如,数字 27,按普遍性递增排序,其类型依次是 fixnum 、 integer 、 rational 、 real 、 number 、 atom 、t。

t 是所有类型的基类,每个对象都属于 t。

谓词 typep 接受一个对象和一个类型,判定对象是否为该类型,如果是,返回真。

1
2
(typep 27 'integer)
> T

展望

lisp 是适合写 lisp 的语言,写代码的时候,不只是在语言中编程,也是在让语言适合程序。

lisp 的语法单一,一切都基于列表。lisp 本身就是 lisp 程序

这一切是 lisp 最最典型,最最优雅的特性。

每一个 lisp 程序员都应当善用 lisp 的这些特性,在适应 lisp 的同时,也让 lisp 适应自己……

花里胡哨的!

习题

  1. 描述下列表达式求值之后的结果:

(a) (+ (- 5 1) (+ 3 7))

(b) (list 1 (+ 2 3))

(c) (if (listp 1) (+ 1 2) (+ 3 4))

(d) (list (and (listp 3) t) (+ 1 2))

1
2
3
4
(a) 14
(b) (1 5)
(c) 7
(d) (NIL 3)
  1. 给出 3 种不同表示 (a b c) 的 cons 表达式。
1
2
3
(cons 'a '(b c))
(cons 'a (cons 'b '(c)))
(cons 'a (cons 'b (cons 'c ())))
  1. 使用 car 与 cdr 来定义一个函数,返回一个列表的第四个元素。
1
2
(defun my-fourth (lst) 
(car (cdr (cdr (cdr lst)))))
  1. 定义一个函数,接受两个实参,返回两者当中较大的那个。
1
2
3
4
(defun my-max (a b)
(if (> a b)
a
b))
  1. 这些函数做了什么?
1
2
3
4
5
6
7
8
9
10
11
12
(a) (defun enigma (x) ; 显然,x 是列表
(and (not (null x)) ; 如果列表为非空,才继续执行
(or (null (car x)) ; 如果第一个元素是 null,则停止执行,否则继续执行
(enigma (cdr x)))))

(b) (defun mystery (x y)
(if (null y)
nil ; 如果 y 为空列表,返回 nil
(if (eql (car y) x) ; 显然,x 是一个值,y 是列表
0 ; 如果有相等,返回 0
(let ((z (mystery x (cdr y))))
(and z (+ z 1)))))) ; 如果 z 为 nil,没得算了。返回 nil,如果非,则返回 nil+1
1
2
3
(a) 故意折腾人呢?
显然,是判断一个列表里是否有 NIL 或 (),如果有,则返回 T,否则返回 NIL
(b) 返回 y 列表中第一个 x 的 index……这个真的难以判断
  1. 下列表达式, x 该是什么,才会得到相同的结果?

(a) > (car (x (cdr ‘(a (b c) d))))
B

(b) > (x 13 (/ 1 0))
13

(c) > (x #’list 1 nil)
(1)

1
2
3
4
5
(a) car

(b) ???

(c) apply
  1. 只使用本章所介绍的操作符,定义一个函数,它接受一个列表作为实参,如果有一个元素是列表时,就返回真,
1
2
3
4
5
(defun my-algo (lst)
(and (null lst) (or (listp (car lst)) (my-listp (cdr lst)))))
(if (null lst)
nil ; 不能手贱写大写的 NIL……草
(or (listp (car lst)) (my-listp (cdr lst)))))
  1. 给出函数的迭代与递归版本。
    a. 接受一个正整数,并打印出数字数量的点。
    b. 接受一个列表,并返回 a 在列表里所出现的次数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
a.
迭代:
(defun print-point (n)
(do ((i 0 (+ i 1))) ((>= i n) 'done)
(format t ".")))

递归:
(defun print-point (n)
(and (not (zerop n))
(progn (format t ".") (print-point (- n 1)))))

b.
迭代:
(defun my-count (lst)
(let ((len 0))
(dolist (obj lst)
(if (equal obj 'a)
(setf len (+ len 1))
nil))
len))

递归:
(defun my-count (lst)
(if (null lst)
0
(if (equal (car lst) 'a)
(+ (my-count (cdr lst)) 1)
(my-count (cdr lst)))))

尾递归:
(defun my-count (count lst)
(if (null lst)
count
(if (equal (car lst) 'a)
(my-count (+ 1 count) (cdr lst))
(my-count count (cdr lst)))))
  1. 一位朋友想写一个函数,返回列表里所有非 nil 元素的和。他写了此函数的两个版本,但两个都不能工作。请解释每一个的错误在哪里,并给出正确的版本。
1
2
3
4
5
6
7
8
9
(a) (defun summit (lst)
(remove nil lst)
(apply #'+ lst))

(b) (defun summit (lst)
(let ((x (car lst)))
(if (null x)
(summit (cdr lst))
(+ x (summit (cdr lst))))))
1
2
3
4
5
6
7
8
9
10
11
12
13
(a) 
(defun summit (lst)
(setf lst (remove nil lst)) ;remove 是无副作用的,要赋值才行
(apply #'+ lst))

(b)
(defun summit (lst)
(if (null lst)
0 ; 递归的出口
(let ((x (car lst)))
(if (null x)
(summit (cdr lst))
(+ x (summit (cdr lst)))))))

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!