elisp 里的对象都是有类型的,而且每一个对象它们知道自己是什么类型。得到一个变量名之后可以用一系列检测方法来测试这个变量是什么类型。

内建的 emacs 数据类型称为 primitive types,包括整数、浮点数、cons、符号(symbol)、字符串、向量(vector)、散列表(hash-table)、subr(内建函数,比如 cons, if, and 之类)、byte-code function,和其它特殊类型,例如缓冲区(buffer)。

数字

emacs 的数字分为整数和浮点数。

1,1.,+1, -1, 536870913, 0, -0 这些都是整数。整数的范围是和机器是有关的,一般来最小范围是在 -268435456 to 268435455(29 位,-2^28 ~ 2^28-1)。可以从 most-positive-fixnummost-negative-fixnum 两个变量得到整数的范围。

你可以用多种进制来输入一个整数。比如:

1
2
3
#b101100 => 44      ; 二进制
#o54 => 44 ; 八进制
#x2c => 44 ; 十进制

最神奇的是你可以用 2 到 36 之间任意一个数作为基数,比如:

1
#24r1k => 44        ; 二十四进制

1500.0, 15e2, 15.0e2, 1.5e3, 和 .15e4 都可以用来表示一个浮点数 1500.。遵循 IEEE 标准,elisp 也有一个特殊类型的值称为 NaN (not-a-number)。你可以用 (/ 0.0 0.0) 产生这个数。

测试函数

整数类型测试函数是 integerp,浮点数类型测试函数是 floatp。数字类型测试用 numberp。

1
2
3
4
5
(integerp 1.)                           ; => t
(integerp 1.0) ; => nil
(floatp 1.) ; => nil
(floatp -0.0e+NaN) ; => t
(numberp 1) ; => t

还提供一些特殊测试,比如测试是否是零的 zerop,还有非负整数测试的 wholenump。

elisp 测试函数一般都是用 p 来结尾,p 是 predicate 的第一个字母。如果函数名是一个单词,通常只是在这个单词后加一个 p,如果是多个单词,一般是加 -p。

数的比较

常用的比较操作符号是我们在其它言中都很熟悉的,比如 <, >, >=, <=,不一样的是,由于赋值是使用 set 函数,所以 = 不再是一个赋值运算符了,而是测试数字相等符号。和其它语言类似,对于浮点数的相等测试都是不可靠的。比如:

1
2
3
(setq foo (- (+ 1.0 1.0e-3) 1.0))       ; => 0.0009999999999998899
(setq bar 1.0e-3) ; => 0.001
(= foo bar) ; => nil

所以一定要确定两个浮点数是否相同,是要在一定误差内进行比较。这里给出一个函数:

1
2
3
4
5
6
7
(defvar fuzz-factor 1.0e-6)
(defun approx-equal (x y)
(or (and (= x 0) (= y 0))
(< (/ (abs (- x y))
(max (abs x) (abs y)))
fuzz-factor)))
(approx-equal foo bar) ; => t

还有一个测试数字是否相等的函数 eql,这是函数不仅测试数字的值是否相等,还测试数字类型是否一致,比如:

1
2
(= 1.0 1)                               ; => t
(eql 1.0 1) ; => nil

elisp 没有 +=, -=, /=, *= 这样的命令式语言里常见符号,如果你想实现类似功能的语句,只能用赋值函数 setq 来实现了。 /= 符号被用来作为不等于的测试了。

数的转换

整数向浮点数转换是通过 float 函数进行的。而浮点数转换成整数有这样几个函数:

  • truncate 转换成靠近~0 的整数
  • floor 转换成最接近的不比本身大的整数
  • ceiling 转换成最接近的不比本身小的整数
  • round 四舍五入后的整数,换句话说和它的差绝对值最小的整数

数的运算

+ - * /

四则运算 有什 好说的,就是 + - * /。值得注意的是,和 C 语言类似,如果参数都是整数,作除法时要记住 (/ 5 6) 是会等于 0 的。如果参数中有浮点数,整数会自动转换成浮点数进行运算,所以 (/ 5 6.0) 的值才会是 5/6。

1+ 和 1-

没有 ++ 和 – 操作了,类似的两个函数是 1+ 和 1-。可以用 setq 赋值来代替 ++ 和 --:

1
2
3
(setq foo 10)                           ; => 10
(setq foo (1+ foo)) ; => 11
(setq foo (1- foo)) ; => 10

abs

取数的绝对值。

% 和 mod

有两个取整的函数,一个是符号 %,一个是函数 mod。这两个函数有什么差别呢?一是 % 的第一个参数必须是整数,而 mod 的第一个参数可以是整数也可以是浮点数。二是即使对相同的参数,两个函数也不一定有相同的返回值:

1
2
(+ (% DIVIDEND DIVISOR)
(* (/ DIVIDEND DIVISOR) DIVISOR))

和 DIVIDEND 是相同的。而:

1
2
(+ (mod DIVIDEND DIVISOR)
(* (floor DIVIDEND DIVISOR) DIVISOR))

和 DIVIDEND 是相同的。

三角运算

三角运算有函数:sin, cos, tan, asin, acos, atan。

乘方和开方

expt 可以指定底数的指数运算。开方函数是 sqrt。exp 是以 e 为底的指数运算,expt 可以指定底数的指数运算。log 默认底数是 e,但是也可以指定底数。log10 就是 (log x 10)。logb 是以 2 为底数运算,但是返回的是一个整数。这个函数是用来计算数的位。

随机数

random 可以产生随机数。可以用 (random t) 来产生一个新种子。虽然 emacs 每次启动后调用 random 总是产生相同的随机数,但是运行过程中,你不知道调用了多少次,所以使用时还是不需要再调用一次 (random t) 来产生新的种子。

函数列表

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
37
38
39
40
41
42
43
44
;; 测试函数
(integerp OBJECT)
(floatp OBJECT)
(numberp OBJECT)
(zerop NUMBER)
(wholenump OBJECT)
;; 比较函数
(> NUM1 NUM2)
(< NUM1 NUM2)
(>= NUM1 NUM2)
(<= NUM1 NUM2)
(= NUM1 NUM2)
(eql OBJ1 OBJ2)
(/= NUM1 NUM2)
;; 转换函数
(float ARG)
(truncate ARG &optional DIVISOR)
(floor ARG &optional DIVISOR)
(ceiling ARG &optional DIVISOR)
(round ARG &optional DIVISOR)
;; 运算
(+ &rest NUMBERS-OR-MARKERS)
(- &optional NUMBER-OR-MARKER &rest MORE-NUMBERS-OR-MARKERS)
(* &rest NUMBERS-OR-MARKERS)
(/ DIVIDEND DIVISOR &rest DIVISORS)
(1+ NUMBER)
(1- NUMBER)
(abs ARG)
(% X Y)
(mod X Y)
(sin ARG)
(cos ARG)
(tan ARG)
(asin ARG)
(acos ARG)
(atan Y &optional X)
(sqrt ARG)
(exp ARG)
(expt ARG1 ARG2)
(log ARG &optional BASE)
(log10 ARG)
(logb ARG)
;; 随机数
(random &optional N)

变量列表

1
2
most-positive-fixnum
most-negative-fixnum

字符和字符串

在 emacs 里字符串是有序的字符数组。和 c 语言的字符串数组不同,emacs 的字符串可以容纳任何字符,包括 \0 :

1
(setq foo "abc\000abc")                 ; => "abc^@abc"

首先构成字符串的字符其实就是一个整数。一个字符 A 就是一个整数 65。但是目前字符串中的字符被限制在 0-524287 之间。字符的读入语法是在字符前加上一个问号,比如 ?A 代表字符 A

1
2
?A                                      ; => 65
?a ; => 97

对于标点来说,也可以用同样的语法,但是最好在前面加上转义字符 \,因为有些标点会有岐义,比如 ?|'。 ` 必须用 ?\\' 表示。控制字符,退格、制表符,换行符,垂直制表符,换页符,空格,回车,删除和 escape 表示为 ?\a, ?\b?\t?\n?\v?\f?\s?\r?\d?\e。对于没有特殊意义的字符,加上转义字符 \ 是没有副作用的,比如 ?\+?+ 是完全一样的。所以标点还是都用转义字符来表示吧。

1
2
3
4
5
6
7
8
9
10
11
?\a => 7                 ; control-g, `C-g'
?\b => 8 ; backspace, <BS>, `C-h'
?\t => 9 ; tab, <TAB>, `C-i'
?\n => 10 ; newline, `C-j'
?\v => 11 ; vertical tab, `C-k'
?\f => 12 ; formfeed character, `C-l'
?\r => 13 ; carriage return, <RET>, `C-m'
?\e => 27 ; escape character, <ESC>, `C-['
?\s => 32 ; space character, <SPC>
?\\ => 92 ; backslash character, `\'
?\d => 127 ; delete character, <DEL>

控制字符可以有多种表示方式,比如 C-i ,这些都是对的:

1
?\^I  ?\^i  ?\C-I  ?\C-i 

它们都对应数字 9。meta 字符是用 <META> 修饰键(通常就是 Alt 键)输入的字符。之所以称为修饰键,是因为这样输入的字符就是在其修饰字符的第 27 位由 0 变成 1 而成,也就是如下操作:

1
2
(logior (lsh 1 27) ?A)                  ; => 134217793
?\M-A ; => 134217793

你可以用 ‘\M-’ 代表 meta 键,加上修饰的字符就是新生成的字符。比如:?\M-A, ?\M-\C-b. 后面这个也可以写成 ?\C-\M-b

如果你还记得前面说过字符串里的字符不能超过 524287 的话,这就可以看出字符串是不能放下一个 meta 字符的。所以按键序列在这时只能用 vector 来储存。

其它的修饰键也是类似的。emacs 用 2^25 位来表示 shift 键,2^24 对应 hyper,2^23 对应 super,2^22 对应 alt。

测试函数

字符串测试使用 stringp(没有 charp,因为字符就是整数)。string-or-null-p 当对象是一个字符或 nil 时返回 t。char-or-string-p 测试是否是字符串或者字符类型。

emacs 没有测试字符串是否为空的函数,可以通过 length 获取字符串的长度来判断是否为一个空字符串。

1
2
(setq a-string nil)
(length a-string) ; => 0

构造函数

产生一个字符串可以用 make-string。这样生成的字符串包含的字符都是一样的。要生成不同的字符串可以用 string 函数。

1
2
(make-string 5 ?x)                      ; => "xxxxx"
(string ?a ?b ?c) ; => "abc"

在已有的字符串生成新的字符串的方法有 substring, concat。substring 的后两个参数是起点和终点的位置。如果终点越界或者终点比起点小都会产生一个错误。这个在使用 substring 时要特别小心。

1
2
3
(substring "0123456789" 3)              ; => "3456789"
(substring "0123456789" 3 5) ; => "34"
(substring "0123456789" -3 -1) ; => "78"

concat 函数相对简单,就是把几个字符串连接起来。

字符串比较

char-equal 可以比较两个字符是否相等。与整数比较不同,这个函数还考虑了大小写。如果 case-fold-search 变量是 t 时,这个函数的字符比较是忽略大小写的。编程时要小心,因为通常 case-fold-search 都是 t,这样如果要考虑字符的大小写时就不能用 char-equal 函数了。

字符串比较使用 string=,string-equal 是一个别名。 string< 是按字典序比较两个字符串,string-less 是它的别名。空字符串小于所有字符串,除了空字符串。没有 string> 函数。

转换函数

字符转换成字符串可以用 char-to-string 函数,字符串转换成字符可以用 string-to-char。当然只是返回字符串的第一个字符。

数字和字符串之间的转换可以用 number-to-string 和 string-to-number。其中 string-to-number 可以设置字符串的进制,可以从 2 到 16。

number-to-string 只能转换成 10 进制的数字。如果要输出八进制或者十六进制,可以用 format 函数:

1
2
3
4
(string-to-number "256")                ; => 256
(number-to-string 256) ; => "256"
(format "%#o" 256) ; => "0400"
(format "%#x" 256) ; => "0x100"

如果要输出成二进制,好像没有现成的函数了。可以利用 calculator 库:

1
2
3
4
5
6
(defun number-to-bin-string (number)
(require 'calculator)
(let ((calculator-output-radix 'bin)
(calculator-radix-grouping-mode nil))
(calculator-number-to-string number)))
(number-to-bin-string 256) ; => "100000000"

concat 可以把一个字符构成的列表或者向量转换成字符串,vconcat 可以把一个字符串转换成一个向 量,append 可以把一个字符串转换成一个列表。

1
2
3
4
(concat '(?a ?b ?c ?d ?e))              ; => "abcde"
(concat [?a ?b ?c ?d ?e]) ; => "abcde"
(vconcat "abdef") ; => [97 98 100 101 102]
(append "abcdef" nil) ; => (97 98 99 100 101 102)

大小写转换使用的是 downcase 和 upcase 两个函数。capitalize 可以使字符串中单词的第一个字符大写,其它字符小写。upcase-initials 只使第一个单词的第一个字符大写,其它字符小写。这两个函数的参数如果是一个字符,那么只让这个字符大写。比如:

1
2
3
4
5
6
(downcase "The cat in the hat")         ; => "the cat in the hat"
(downcase ?X) ; => 120
(upcase "The cat in the hat") ; => "THE CAT IN THE HAT"
(upcase ?x) ; => 88
(capitalize "The CAT in tHe hat") ; => "The Cat In The Hat"
(upcase-initials "The CAT in the hAt") ; => "The CAT In The HAt"

格式化字符串

format 类似于 C 语言里的 printf 可以实现对象的字符串化。数字的格式化和 printf 的参数差不多,值得一提的是 “%S” 这个格式化形式,它可以把对象的输出形式转换成字符串,这在调试时是很有用的。

查找和替换

查找

字符串查找的核心函数是 string-match。这个函数可以从指定的位置对字符串进行正则表达式匹配,如果匹配成功,则返回匹配的起点,如:

1
2
(string-match "34" "01234567890123456789")    ; => 3
(string-match "34" "01234567890123456789" 10) ; => 13

注意 string-match 的参数是一个正则式。如果想把 string-match 作为一个查找子串的函数,可以先用 regexp-quote 函数先处理一下子串。比如:

1
2
(string-match "2* "232*3=696")			; => 0
(string-match (regexp-quote "2*") "232*3=696") ; => 2

事实上,string-match 不只是查找字符串,它更重要的功能是捕捉匹配的字符串。string-match 在查找的同时,还会记录下每个要捕捉的字符串的位置。这个位置可以在匹配后用 match-data、match-beginning 和 match-end 等函数来获得。先看一下例子:

1
2
3
(progn
(string-match "3\\(4\\)" "01234567890123456789")
(match-data)) ; => (3 5 4 5)

最后返回这个数字是什么意思呢?正则表达式捕捉的字符串按括号的顺序对应一个序号,整个模式对应序号 0,第一个括号对应序号 1,第二个括号对应序号 2,以此类推。所以 “3\(4\)” 这个正则表达式中有序号 0 和 1,最后 match-data 返回的一系列数字对应的分别是要捕捉字符串的起点和终点位置,也就是说子串 “34” 起点从位置 3 开始,到位置 5 结束,而捕捉的字符串 “4” 的起点是从 4 开始,到 5 结束。这些位置可以用 match-beginning 和 match-end 函数用对应的序号得到。

要注意的是,起点位置是捕捉字符串的第一个字符的位置,而终点位置不是捕捉的字符串最后一个字符的位置,而是下一个字符的位置。这个性质对于循环是很方便的。比如要查找上面这个字符串中所有 34 出现的位置:

1
2
3
4
(let ((start 0))
(while (string-match "34" "01234567890123456789" start)
(princ (format "find at %d\n" (match-beginning 0)))
(setq start (match-end 0))))

替换

替换使用的函数是 replace-match。这个函数既可以用于字符串的替换,也可以用于缓冲区的文本替换。对于字符串的替换,replace-match 只是按给定的序号把字符串中的那一部分用提供的字符串替换了而已:

1
2
3
4
5
(let ((str "01234567890123456789"))
(string-match "34" str)
(princ (replace-match "x" nil nil str 0))
(princ "\n")
(princ str))

可以看出 replace-match 返回的字符串是替换后的新字符串,原字符串被没有改变。

函数列表

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
;; 测试函数
(stringp OBJECT)
(string-or-null-p OBJECT)
(char-or-string-p OBJECT)
;; 构建函数
(make-string LENGTH INIT)
(string &rest CHARACTERS)
(substring STRING FROM &optional TO)
(concat &rest SEQUENCES)
;; 比较函数
(char-equal C1 C2)
(string= S1 S2)
(string-equal S1 S2)
(string< S1 S2)
;; 转换函数
(char-to-string CHAR)
(string-to-char STRING)
(number-to-string NUMBER)
(string-to-number STRING &optional BASE)
(downcase OBJ)
(upcase OBJ)
(capitalize OBJ)
(upcase-initials OBJ)
(format STRING &rest OBJECTS)
;; 查找与替换
(string-match REGEXP STRING &optional START)
(replace-match NEWTEXT &optional FIXEDCASE LITERAL STRING SUBEXP)
(replace-regexp-in-string REGEXP REP STRING &optional FIXEDCASE LITERAL SUBEXP START)
(subst-char-in-string FROMCHAR TOCHAR STRING &optional INPLACE)

cons cell 和列表

cons cell

cons cell 就是两个有顺序的元素。第一个叫 CAR,第二个叫 CDR。

  • CAR 指令用于取出地址部分,表示(Contents of Address part of Register);
  • CDR 指令用于取出地址的减量部分 (Contents of the Decrement part of Register)。

cons cell 的读入语法是用 . 分开两个部分,比如:

1
2
3
4
5
'(1 . 2)                                ; => (1 . 2)
'(?a . 1) ; => (97 . 1)
'(1 . "a") ; => (1 . "a")
'(1 . nil) ; => (1)
'(nil . nil) ; => (nil)

前面的 ' 是引用(quote)符号,它的作用是将它的参数返回而不作求值。

列表

列表包括了 cons cell。但是列表中有一个特殊的元素──空表 nil。

1
2
nil                                     ; => nil
'() ; => nil

空表不是一个 cons cell,因为它没有 car 和 cdr 两个部分,事实上空表里没有任何内容。但是为了编程的方便,可以认为 nil 的 car 和 cdr 都是 nil:

1
2
(car nil)                               ; => nil
(cdr nil) ; => nil

按列表最后一个 cons cell 的 cdr 部分的类型分,可以把列表分为三类。如果它是 nil 的话,这个列表也称为“真列表”(true list)。如果既不是 nil 也不是一个 cons cell,则这个列表称为“点列表”(dotted list)。还有一种可能,它指向列表中之前的一个 cons cell,则称为环形列表 (circular list)。 这里分别给出一个例子:

1
2
3
'(1 2 3 . nil)                          ; => (1 2 3)
'(1 2 . 3) ; => (1 2 . 3)
'(1 . #1=(2 3 . #1#)) ; => (1 2 3 . #1)

从这个例子可以看出前两种列表的读入语法和输出形式都是相同的,而环形列表的读入语法是很古怪的,输出形式不能作为环形列表的读入形式。

测试函数

测试一个对象是否是 cons cell 用 consp,是否是列表用 listp。

1
2
3
4
5
6
(consp '(1 . 2))                        ; => t
(consp '(1 . (2 . nil))) ; => t
(consp nil) ; => nil
(listp '(1 . 2)) ; => t
(listp '(1 . (2 . nil))) ; => t
(listp nil) ; => t

没有内建的方法测试一个列表是不是一个真列表。通常如果一个函数需要一个真列表作为参数,都是在运行时发出错误,而不是进行参数检查,因为检查一个列表是真列表的代价比较高。

测试一个对象是否是 nil 用 null 函数。只有当对象是空表时,null 才返回空值。

构造函数

生成一个 cons cell 可以用 cons 函数。比如:

1
2
(cons 1 2)                              ; => (1 . 2)
(cons 1 '()) ; => (1)

这也是在列表前面增加元素的方法。比如:

1
2
(setq foo '(a b))                       ; => (a b)
(cons 'x foo) ; => (x a b)

生成一个列表的函数是 list。比如:

1
2
(list 1 2 3)                            ; => (1 2 3)
(list 'a 'b 'c) ; => (a b c)

在列表后端增加元素的函数是用 append。比如:

1
2
(append '(a b) '(c))                    ; => (a b c)
(append '(a b) '(c) '(d)) ; => (a b c d)

append 的功能可以认为它是把第一个参数最后一个列表的 nil 换成第二个参数。一般来说 append 的参数都要是列表,但是最后一个参数可以不是一个列表,这也不违背前面说的,因为 cons cell 的 CDR 部分本来就可以是任何对象:

1
(append '(a b) 'c)                      ; => (a b . c)

这样得到的结果就不再是一个真列表了,如果再进行 append 操作就会产生一个错误。

把列表当数组用

要得到列表或者 cons cell 里元素,可以使用 car 和 cdr 函数。很容易明白,car 就是取得 cons cell 的 CAR 部分(第一个元素),cdr 函数就是取得 cons cell 的 CDR 部分(第二个元素)。再往后就没有这样的函数了,可以用 nth 函数来访问:

1
2
3
(car '(0 1 2 3 4 5))                    ; => 0
(cdr '(0 1 2 3 4 5)) ; => (1 2 3 4 5)
(nth 3 '(0 1 2 3 4 5)) ; => 3

获得列表一个区间的函数有 nthcdr、last 和 butlast。nthcdr 和 last 比较类似,它们都是返回列表后端的列表。

nthcdr 函数返回第 n 个元素后的列表:

1
(nthcdr 2 '(0 1 2 3 4 5))               ; => (2 3 4 5)

last 函数返回倒数 n 个长度的列表:

1
(last '(0 1 2 3 4 5) 2)                 ; => (4 5)

butlast 和前两个函数不同,返回的除了倒数 n 个元素的列表。

1
(butlast '(0 1 2 3 4 5) 2)              ; => (0 1 2 3)
以上所列举的几个函数都是非破坏性的——也就是说,它们不改变所作用的列表。

setcar 和 setcdr 可以修改一个 cons cell 的 CAR 部分和 CDR 部分。比如:

1
2
3
4
5
(setq foo '(a b c))                     ; => (a b c)
(setcar foo 'x) ; => x
foo ; => (x b c)
(setcdr foo '(y z)) ; => (y z)
foo ; => (x y z)

使用 setcar 和 nthcdr 的组合可以修改列表中的任一元素:

1
2
3
4
5
(setq foo '(1 2 3))                     ; => (1 2 3)
(setcar foo 'a) ; => a
(setcar (cdr foo) 'b) ; => b
(setcar (nthcdr 2 foo) 'c) ; => c
foo ; => (a b c)

把列表当堆栈用

push 和 pop 可以把列表当做堆栈来使用:分别实现堆栈的入栈和出栈操作:

1
2
3
4
5
(setq foo nil)                          ; => nil
(push 'a foo) ; => (a)
(push 'b foo) ; => (b a)
(pop foo) ; => b
foo ; => (a)

重排列表

reverse 函数和 nreverse 可以将列表反序,两者的区别在于一个是非破坏性的,一个是破坏性的:

1
2
3
4
5
(setq foo '(a b c))                     ; => (a b c)
(reverse foo) ; => (c b a)
foo ; => (a b c)
(nreverse foo) ; => (c b a)
foo ; => (a)

为什么现在 foo 指向的是列表的末端呢?如果你实现过链表就知道,逆序操作是可以在原链表上进行的,这样原来头部指针会变成链表的尾端。

elisp 还有一些是具有破坏性的函数。最常用的就是 sort 函数:

1
2
3
(setq foo '(3 2 4 1 5))                 ; => (3 2 4 1 5)
(sort foo '<) ; => (1 2 3 4 5)
foo ; => (3 4 5)

如果既要保留原列表,又要进行 sort 操作,可以用 copy-sequence 函数。这个函数只对列表进行复制,返回的列表的元素还是原列表里的元素,不会拷贝列表的元素。

1
2
3
(setq foo '(3 2 4 1 5))                 ; => (3 2 4 1 5)
(sort (copy-sequence foo) '<) ; => (1 2 3 4 5)
foo ; => (3 2 4 1 5)

nconc 和 append 功能相似,但是它会修改除最后一个参数以外的所有的参数, nbutlast 和 butlast 功能相似,也会修改参数。这些函数都是在效率优先时才使用。总而言之,以 n 开头的函数都要慎用。

以 n 开头的函数大多是破坏性的,都要慎用。

把列表当集合用

列表可以作为无序的集合。合并集合用 append 函数。去除重复的 equal 元素用 delete-dups。查找一个元素是否在列表中,如果测试函数是用 eq,就用 memq,如果测试用 equal,可以用 member。删除列表中的指定的元素,测试函数为 eq 对应 delq 函数,equal 对应 delete。还有两个函数 remq 和 remove 也是删除指定元素。它们的差别是 delq 和 delete 可能会修改参数,而 remq 和 remove 总是返回删除后列表的拷贝。注意前面这是说的是可能会修改参数的值,也就是说可能不会,所以保险起见,用 delq 和 delete 函数要么只用返回值,要么用 setq 设置参数的值为返回值。

1
2
3
4
5
6
7
(setq foo '(a b c))                     ; => (a b c)
(remq 'b foo) ; => (a c)
foo ; => (a b c)
(delq 'b foo) ; => (a c)
foo ; => (a c)
(delq 'a foo) ; => (c)
foo ; => (a c)

把列表当关联表

在 elisp 编程中,列表最常用的形式应该是作为一个关联表了。所谓关联表,就是可以用一个字符串(通常叫关键字,key)来查找对应值的数据结构。由列表实现的关联表有一个专门的名字叫 association list。在 association list 中关键字是放在元素的 CAR 部分,与它对应的数据放在这个元素的 CDR 部分。根据比较方法的不同,有 assq 和 assoc 两个函数,它们分别对应查找使用 eq 和 equal 两种方法。例如:

1
2
(assoc "a" '(("a" 97) ("b" 98)))        ; => ("a" 97)
(assq 'a '((a . 97) (b . 98))) ; => (a . 97)

通常我们只需要查找对应的数据,所以一般来说都要用 cdr 来得到对应的数据:

1
2
(cdr (assoc "a" '(("a" 97) ("b" 98))))  ; => (97)
(cdr (assq 'a '((a . 97) (b . 98)))) ; => 97

assoc-default 可以一步完成这样的操作:

1
(assoc-default "a" '(("a" 97) ("b" 98)))          ; => (97)

如果查找用的键值(key)对应的数据也可以作为一个键值的话,还可以用 rassoc 和 rassq 来根据数据查找键值:

1
2
(rassoc '(97) '(("a" 97) ("b" 98)))     ; => ("a" 97)
(rassq '97 '((a . 97) (b . 98))) ; => (a . 97)

如果要修改关键字对应的值,最省事的作法就是用 cons 把新的键值对加到列表的头端。但是这会让列表越来越长,浪费空间。如果要替换已经存在的值,一个想法就是用 setcdr 来更改键值对应的数据。但是在更改之前要先确定这个键值在对应的列表里,否则会产生一个错误。另一个想法是用 assoc 查找到对应的元 素,再用 delq 删除这个数据,然后用 cons 加到列表里:

1
2
3
4
5
6
7
8
9
10
(setq foo '(("a" . 97) ("b" . 98)))     ; => (("a" . 97) ("b" . 98))

;; update value by setcdr
(if (setq bar (assoc "a" foo))
(setcdr bar "this is a")
(setq foo (cons '("a" . "this is a") foo))) ; => "this is a"
foo ; => (("a" . "this is a") ("b" . 98))
;; update value by delq and cons
(setq foo (cons '("a" . 97)
(delq (assoc "a" foo) foo))) ; => (("a" . 97) ("b" . 98))

如果不对顺序有要求的话,推荐用后一种方法吧。这样代码简洁,而且让最近更新的元素放到列表前端,查找更快。

把列表当树用

列表的第一个元素如果作为结点的数据,其它元素看作是子节点,就是一个树了。

遍历列表

遍历列表最常用的函数就是 mapc 和 mapcar 了。它们的第一个参数都是一个函数,这个函数只接受一个参数,每次处理一个列表里的元素。这两个函数唯一的差别是前者返回的还是输入的列表,而 mapcar 返回的函数返回值构成的列表:

1
2
(mapc '1+ '(1 2 3))                     ; => (1 2 3)
(mapcar '1+ '(1 2 3)) ; => (2 3 4)

另一个比较常用的遍历列表的方法是用 dolist。它的形式是:

1
(dolist (var list [result]) body...)

其中 var 是一个临时变量,在 body 里可以用来得到列表中元素的值。使用 dolist 的好处是不用写 lambda 函数。一般情况下它的返回值是 nil,但是你也可以指定一个值作为返回值:

1
2
3
4
5
(dolist (foo '(1 2 3))
(incf foo)) ; => nil
(setq bar nil)
(dolist (foo '(1 2 3) bar)
(push (incf foo) bar)) ; => (4 3 2)

其他常用函数

产生数列常用的方法是用 number-sequence 。

1
(number-sequence 0 10 2)                ; => (0 2 4 6 8 10)

解析文本时一个很常用的操作是把字符串按分隔符分解,可以用 split-string 函数:

1
(split-string "key = val" "\\s-*=\\s-*")  ; => ("key" "val")

与 split-string 对应是把几个字符串用一个分隔符连接起来,这可以用 mapconcat 完成。比如:

1
(mapconcat 'identity '("a" "b" "c") "\t") ; => "a	b	c"

identity 是一个特殊的函数,它会直接返回参数。mapconcat 第一个参数是一个函数,可以很灵活的使用。

函数列表

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
;; 列表测试
(consp OBJECT)
(listp OBJECT)
(null OBJECT)
;; 列表构造
(cons CAR CDR)
(list &rest OBJECTS)
(append &rest SEQUENCES)
;; 访问列表元素
(car LIST)
(cdr LIST)
(cadr X)
(caar X)
(cddr X)
(cdar X)
(nth N LIST)
(nthcdr N LIST)
(last LIST &optional N)
(butlast LIST &optional N)
;; 修改 cons cell
(setcar CELL NEWCAR)
(setcdr CELL NEWCDR)
;; 列表操作
(push NEWELT LISTNAME)
(pop LISTNAME)
(reverse LIST)
(nreverse LIST)
(sort LIST PREDICATE)
(copy-sequence ARG)
(nconc &rest LISTS)
(nbutlast LIST &optional N)
;; 集合函数
(delete-dups LIST)
(memq ELT LIST)
(member ELT LIST)
(delq ELT LIST)
(delete ELT SEQ)
(remq ELT LIST)
(remove ELT SEQ)
;; 关联列表
(assoc KEY LIST)
(assq KEY LIST)
(assoc-default KEY ALIST &optional TEST DEFAULT)
(rassoc KEY LIST)
(rassq KEY LIST)
;; 遍历函数
(mapc FUNCTION SEQUENCE)
(mapcar FUNCTION SEQUENCE)
(dolist (VAR LIST [RESULT]) BODY...)
;; 其它
(number-sequence FROM &optional TO INC)
(split-string STRING &optional SEPARATORS OMIT-NULLS)
(mapconcat FUNCTION SEQUENCE SEPARATOR)
(identity ARG)

序列和数组

序列是列表和数组的统称,也就是说列表和数组都是序列。它们的共性是内部的元素都是有序的。elisp 里的数组包括字符串、向量、char-table 和布尔向量。它们的关系可以用下面图表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 _____________________________________________
| |
| Sequence |
| ______ ________________________________ |
| | | | | |
| | List | | Array | |
| | | | ________ ________ | |
| |______| | | | | | | |
| | | Vector | | String | | |
| | |________| |________| | |
| | ____________ _____________ | |
| | | | | | | |
| | | Char-table | | Bool-vector | | |
| | |____________| |_____________| | |
| |________________________________| |
|_____________________________________________|

数组有这样一些特性:

  • 数组内的元素都对应一个下标,第一个元素下标为 0,接下来是 1。数组内的元素可以在常数时间内访问。
  • 数组在创建之后就无法改变它的长度。
  • 数组是自求值的。
  • 数组里的元素都可以用 aref 来访问,用 aset 来设置。

向量可以看成是一种通用的数组,它的元素可以是任意的对象。而字符串是一种特殊的数组,它的元素只能是字符。如果元素是字符时,使用字符串相比向量更好,因为字符串需要的空间更少(只需要向量的 1/4),输出更直观,能用文本属性(text property),能使用 emacs 的 IO 操作。但是有时必须使用向量,比如存储按键序列。

测试函数

sequencep 用来测试一个对象是否是一个序列。arrayp 测试对象是否是数组。vectorp、char-table-p 和 bool-vector-p 分别测试对象是否是向量、 char-table、bool-vector。

序列的通用函数

一直没有提到一个重要的函数 length,它可以得到序列的长度。但是这个函数只对真列表有效。对于一个点列表和环形列表这个函数就不适用了。点列表会出参数类型不对的错误,而环形列表就更危险,会陷入死循环。如果不确定参数类型,不妨用 safe-length。比如:

1
2
(safe-length '(a . b))                  ; => 1
(safe-length '#1=(1 2 . #1#)) ; => 3

取得序列里第 n 个元素可以用 elt 函数。但是我建议,对于已知类型的序列,还是用对应的函数比较好。也就是说,如果是列表就用 nth,如果是数组就用 aref。这样一方面是省去 elt 内部的判断,另一方面读代码时能很清楚知道序列的类型。

copy-sequence 在前面已经提到了。不过同样 copy-sequence 不能用于点列表和环形列表。对于点列表可以用 copy-tree 函数。环形列表就没有办法复制了。好在这样的数据结构很少用到。

数组操作

创建字符串已经说过了。创建向量可以用 vector 函数:

1
(vector 'foo 23 [bar baz] "rats")

当然也可以直接用向量的读入语法创建向量,但是由于数组是自求值的,所以这样得到的向量和原来是一样的,也就是说参数不进行求值,看下面的例子就明白了:

1
2
3
foo                                     ; => (a b)
[foo] ; => [foo]
(vector foo) ; => [(a b)]

用 make-vector 可以生成元素相同的向量。

1
(make-vector 9 'Z)                      ; => [Z Z Z Z Z Z Z Z Z]

fillarray 可以把整个数组用某个元素填充。

1
(fillarray (make-vector 3 'Z) 5)        ; => [5 5 5]

aref 和 aset 可以用于访问和修改数组的元素。如果使用下标超出数组长度的话,会产生一个错误。所以要先确定数组的长度才能用这两个函数。

vconcat 可以把多个序列用 vconcat 连接成一个向量。但是这个序列必须是真列表。这也是把列表转换成向量的方法。

1
(vconcat [A B C] "aa" '(foo (6 7)))     ; => [A B C 97 97 foo (6 7)]

函数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;; 测试函数
(sequencep OBJECT)
(arrayp OBJECT)
(vectorp OBJECT)
(char-table-p OBJECT)
(bool-vector-p OBJECT)
;; 序列函数
(length SEQUENCE)
(safe-length LIST)
(elt SEQUENCE N)
(copy-sequence ARG)
(copy-tree TREE &optional VECP)
;; 数组函数
(vector &rest OBJECTS)
(make-vector LENGTH INIT)
(aref ARRAY IDX)
(aset ARRAY IDX NEWELT)
(vconcat &rest SEQUENCES)
(append &rest SEQUENCES)

符号

符号是有名字的对象。

首先必须知道的是符号的命名规则。符号名字可以含有任何字符。大多数的符号名字只含有字母、数字和标点 -+=*/ 。这样的名字不需要其它标点。名字前缀要足够把符号名和数字区分开来,如果需要的话,可以在前面用 \ 表示为符号,比如:

1
2
3
(symbolp '+1)                           ; => nil
(symbolp '\+1) ; => t
(symbol-name '\+1) ; => "+1"

其它字符 _~!@$%^&amp;:<>{}? 用的比较少。但是也可以直接作为符号的名字。任何其它字符都可以用 \ 转义后用在符号名字里。但是和字符串里字符表示不同,\ 转义后只是表示其后的字符,比如 \t 代表的字符 t,而不是制表符。如果要在符号名里使用制表符,必须在 \ 后加上制表符本身。

符号名是区分大小写的。这里有一些符号名的例子:

1
2
3
4
5
6
7
8
foo                 ; 名为 `foo' 的符号
FOO ; 名为 `FOO' 的符号,和 `foo' 不同
char-to-string ; 名为 `char-to-string' 的符号
1+ ; 名为 `1+' 的符号 (不是整数 `+1')
\+1 ; 名为 `+1' 的符号 (可读性很差的名字)
\(*\ 1\ 2\) ; 名为 `(* 1 2)' 的符号 (更差劲的名字).
+-*/_~!@$%^&=:<>{} ; 名为 `+-*/_~!@$%^&=:<>{}' 的符号.
; 这些字符无须转义

创建符号

符号名要有唯一性,所以一定会有一个表与名字关联,这个表在 elisp 里称为 obarray。当 emacs 创建一个符号时,首先会对这个名字求 hash 值以得到一个在 obarray 这个向量中查找值所用的下标。hash 是查找字符串的很有效的方法。也可以自己建立向量,把这个向量作为 obarray 来使用。这是一种代替散列的一种方法。它比直接使用散列有这样一些好处:

  • 符号不仅可以有一个值,还可以用属性列表,后者又可以相当于一个关联列表。这样有很高的扩展性,而且可以表达更高级的数据结构。
  • emacs 里有一些函数可以接受 obarray 作为参数,比如补全相关的函数。

当 lisp 读入一个符号时,通常会先查找这个符号是否在 obarray 里出现过,如果没有则会把这个符号加入到 obarray 里。这样查找并加入一个符号的过程称为是 intern。intern 函数可以查找或加入一个名字到 obarray 里,返回对应的符号。默认是全局的 obarray,也可以指定一个 obarray。intern-soft 与 intern 不同的是,当名字不在 obarray 里时,intern-soft 会返回 nil,而 intern 会加入到 obarray 里。

1
2
3
4
(setq foo (make-vector 10 0))           ; => [0 0 0 0 0 0 0 0 0 0]
(intern-soft "abc" foo) ; => nil
(intern "abc" foo) ; => abc
(intern-soft "abc" foo) ; => abc

lisp 每读入一个符号都会 intern 到 obarray 里,如果想避免,可以用在符号名前加上 #:

1
2
3
4
5
6
(intern-soft "abc")                     ; => nil
'abc ; => abc
(intern-soft "abc") ; => abc
(intern-soft "abcd") ; => nil
'#:abcd ; => abcd
(intern-soft "abcd") ; => nil

如果想除去 obarray 里的符号,可以用 unintern 函数。unintern 可以用符号名或符号作参数在指定的 obarray 里去除符号,成功去除则返回 t,如果没有查找到对应的符号则返回 nil:

1
2
3
(intern-soft "abc" foo)                 ; => abc
(unintern "abc" foo) ; => t
(intern-soft "abc" foo) ; => nil

和 hash-table 一样,obarray 也提供一个 mapatoms 函数来遍历整个~obarray。比如要计算~obarray 里所有的符号数量:

1
2
3
4
5
6
(setq count 0)                          ; => 0
(defun count-syms (s)
(setq count (1+ count))) ; => count-syms
(mapatoms 'count-syms) ; => nil
count ; => 28371
(length obarray) ; => 1511

符号的组成

每个符号可以对应四个组成部分,一是符号的名字,可以用 symbol-name 访问。二是符号的值。符号的值可以通过 set 函数来设置,用 symbol-value 来访问。

1
2
(set (intern "abc" foo) "I'm abc")      ; => "I'm abc"
(symbol-value (intern "abc" foo)) ; => "I'm abc"

如果一个符号的值已经有设置过的话,则 boundp 测试返回 t,否则为 nil。对于 boundp 测试返回 nil 的符号,使用符号的值会引起一个变量值为 void 的错误。

符号的第三个组成部分是函数。它可以用 symbol-function 来访问,用 fset来设置:

1
2
(fset (intern "abc" foo) (symbol-function 'car)) ; => #<subr car>
(funcall (intern "abc" foo) '(a . b)) ; => a

类似的,可以用 fboundp 测试一个符号的函数部分是否有设置。

符号的第四个组成部分是属性列表(property list)。通常属性列表用于存储和符号相关的信息,比如变量和函数的文档,定义的文件名和位置,语法类型。属性名和值可以是任意的 lisp 对象,但是通常名字是符号,可以用 get 和 put来访问和修改属性值,用 symbol-plist 得到所有的属性列表:

1
2
3
(put (intern "abc" foo) 'doc "this is abc")      ; => "this is abc"
(get (intern "abc" foo) 'doc) ; => "this is abc"
(symbol-plist (intern "abc" foo)) ; => (doc "this is abc")

关联列表和属性列表很相似。符号的属性列表在内部表示上是用 (prop1 value1 prop2 value2 …) 的形式,和关联列表也是很相似的。属性列表在查找和这个符号相关的信息时,要比直接用关联列表要简单快捷的多。所以变量的文档等信息都是放在符号的属性列表里。但是关联表在头端加入元素是很快的,而且它可以删除表里的元素。而属性列表则不能删除一个属性。

如果已经把属性列表取出,那么还可以用 plist-get 和 plist-put 的方法来访问和设置属性列表:

1
2
3
4
5
(plist-get '(foo 4) 'foo)               ; => 4
(plist-get '(foo 4 bad) 'bar) ; => nil
(setq my-plist '(bar t foo 4)) ; => (bar t foo 4)
(setq my-plist (plist-put my-plist 'foo 69)) ; => (bar t foo 69)
(setq my-plist (plist-put my-plist 'quux '(a))) ; => (bar t foo 69 quux (a))

函数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(symbolp OBJECT)
(intern-soft NAME &optional OBARRAY)
(intern STRING &optional OBARRAY)
(unintern NAME &optional OBARRAY)
(mapatoms FUNCTION &optional OBARRAY)
(symbol-name SYMBOL)
(symbol-value SYMBOL)
(boundp SYMBOL)
(set SYMBOL NEWVAL)
(setq SYM VAL SYM VAL ...)
(symbol-function SYMBOL)
(fset SYMBOL DEFINITION)
(fboundp SYMBOL)
(symbol-plist SYMBOL)
(get SYMBOL PROPNAME)
(put SYMBOL PROPNAME VALUE)

Comments