Emacs Lisp 教程
声明
学习自happierbee写的教程.
执行程序
在*scratch*中, 写完一行代码后, 在代码最后面键入C-j, 或者C-x C-e.
基础知识
函数和变量
函数
函数的例子:
(defun hello-world (name) "say hello to user whose name is NAME." (message "Hello, %s" name)) ;; 调用 (hello-world "Emacser")
函数的返回值是函数里的最后一个表达式.
全局变量
由于Elisp中函数是全局的, 所以变量也很容易成为全局变量.
- setq
(setq foo "I am Emacser") ;; "I am Emacser" (message foo) ;; "I am Emacser"
- defvar
defvar也可以定义变量, 但是如果定义的变量在之前有赋过值, 则不起作用. 另外, defvar可以为变量提供文档字符串, 即可以使用C-h v来查看变量的说明.
(defvar variable-name value "document string") ;; 例子 (defvar foo "Did I have a value" "A demo variable") (message foo) ;; "I am Emacser", 因为之前已经用setq对foo赋过值 (defvar bar "I am bar" "A demo variable name") (message bar) ;; "I am bar", 由于之前没有对bar赋值, 因此本次生效
局部变量
Elisp中使用 let
或 let*
来对局部变量进行绑定.
(defun circle-area (radix) (let ((pi 3.1415926) area) (setq area (* pi radix radix)) (message "直径为 %.2f 的圆面积是 %.2f" radix area))) (circle-area 3) ;; 或者 (defun circle-area (radix) (let* ((pi 3.1415926) (area (* pi radix radix))) (message "直径为 %.2f 的圆面积是 %.2f" radix area)))
let*和let的使用形式完全相同, 区别在于let*在声明中就能使用前面声明的变量.
lambda表达式
(lambda (arguments-list) "documentation string" body) ;; 调用 (funcall (lambda (name) (message "Hello %s" name)) "Emacser")
也可以把lambda表达式赋值给一个变量, 再用funcall来调用:
(setq foo (lambda (name) (message "Hello %s" name))) (funcall foo "Emacser")
控制结构
顺序执行
使用progn.
条件判断
(if condition true_body false_body)
还有一个条件判断的方法, 有点像C中的switch-case, 结构如下:
(cond (case1 body) (case2 body) ... (t body)) ;; 前面的情况都不符合时, 执行这条语句 ;; 例子 (defun fib (n) (cond ((= n 0) 0) ((= n 1) 1) (t (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ;; returns 55
循环
(while condition body)
逻辑运算
and和or具有短路性质, or常用于设置函数的默认参数, and用于参数检查. 如:
(defun hello-world (&optional name) (or name (setq name "Emacser")) (message "Hello %s" name)) (hello-world) ;; "Hello Emacser" (hello-world "Vim") ;; "Hello Vim"
(defun square-number-p (n) (and (>= n 0) (= (/ n (sqrt n)) (sqrt n)))) ;; (n >= 0) && (n/sqrt(n)) == (sqrt(n)) (square-number-p -1) ;; nil (square-number-p 25) ;; t
数字
emacs的数字分为整数和浮点数.
测试函数
是否为整数类型: integerp 是否为浮点数类型: floatp 是否为数字类型: numberp 是否为0: zerop 是否为非负整数: wholenump
数的比较
由于Elisp中的赋值是setq函数, 所以=就是比较两个数字是否相等. 还有一些跟比较有关的操作符: >, <, >=, <=
由于精度的原因, 如果比较两个浮点数, 一般结果都是不相等, 正确的比较, 应该在一定的误差内进行比较.
eql可以比较两个数字的值和类型是否都一致.
注意, 不等号是/=.
数的转换
整数->浮点数: float 浮点数->整数: 向上取整(ceiling), 向下取整(floor), 四舍五入(round)
数的运算
与其他语言类似. 没有++和--, 可以这样写: (setq foo (1+ foo))和(setq foo (1- foo)) 取余: %或mod函数, %要求第1个参数为整数, 而mod则没有这个要求 绝对值: abs 三角函数: sin, cos, tan, asin, acos, atan 开方: sqrt 指数: exp是以e为底的指数运算, expt可以自己指定底数 对数: log, 底数默认为e, 也可以自己指定 (log arg &optional base) 随机数: random, (random t)可以产生新种子
字符和字符串
Elisp中的字符串是有序的字符数组, 和C不同的是, Elisp中的字符串可以容纳任何字符, 包括\0.
字符
字符的读入语法, 是在字符前加问号:
?A ;; 65 ?\a ;; 转义字符, 7 ?\C-i ;; 表示键入的Ctrl-i, 9 ?\M-A ;; 表示键入的Alt-A
测试函数
是否为字符串: stringp; 没有charp, 因为字符就是整数. string-or-null-p: 对象是一个字符或nil时, 返回t char-or-string-p: 对象是否为字符串或字符
Elisp没有测试字符串是否为空的函数, 需要自定义:
(defun string-emptyp (str) (not (string< "" str)))
构造函数
(make-string 5 ?x) ;; "xxxxx" (string ?a ?b ?c) ;; "abc" (substring "0123456789" 3 5) ;; "34" (concat "0" "1") ;; "01"
字符串比较
char-equal: 比较两个字符是否相等. 通常case-fold-search都是t, 表示忽略大小写 string=: 字符串比较; string-equal是别名 string<: 按字典序比较, string-less是别名 空字符串小于所有字符串, length可以检测字符串长度, 所以也可以用length来判断字符串是否为空.
转换函数
string-to-char: 只返回字符串的第一个字符 char-to-string: 字符转字符串 string-to-number number-to-string: 只能转10进制的数字, 若要输出其他进制, 可以用format函数, (format "%#o" 256) concat: 可以把一个字符构成的列表或向量转成字符串 vconcat: 可以把字符串转成列表 downcase/upcase: 大小写转换 capitalize: 第1个字符大写, 其他小写 upcase-initials: 第1个字符大写, 其他不管
查找和替换
(string-match regexp string &optional start): 从指定位置对字符串进行正则表达式匹配.
有时需要对正则表达式进行处理:
(string-match "2*" "232*3=696") ;; 0 (string-match (regexp-quote "2*") "232*3=696") ;; 2
(replace-match newtext &optional fixedcase literal string subexp): 替换函数 如: (replace-match "x" nil nil str 0)
cons cell和列表
cons cell是一种数据结构, 仅包含两个元素, 第一个叫CAR, 第二个叫CDR. CAR和CDR可以引用任何对象.
读入cons cell
'(1 . 2) ;; (1 . 2)
cons cell前面有个单引号的意思: eval-last-sexp的步骤: 读入前一个S-表达式, 然后对这个表达式求值. 数字和字符串是一类特殊的S-表达式, 它们求值前和求值后都不变, 也称为自求值表达式. '其实是quote函数, 它的作用是将参数返回, 而不求值.
列表和cons cell的关系
列表 = cons cell + 空表
'() ;; nil
空表不是cons cell, 因为它没有CAR和CDR两个部分. 如果一个cons cell为(1 . nil), 则可以简写成(1).
假如有以下cons cell:
'(1 . (2 . (3 . nil))) ;; (1 2 3)
可以看出, 这个cons cell内部又嵌套了两个cons cell. 读入后输出是一个列表.
测试函数
(consp '(1 . 3)) ;; t (consp '(1 3)) ;; t (consp '(1 3 4)) ;; t (consp nil) ;; nil (listp '(1 3 4)) ;; t
构造函数
生成一个cons cell可以用cons函数.
(cons 1 3) ;; (1 . 3)
在列表前面增加元素:
(setq foo '(a b)) ;; (a b) (cons 'x foo) ;; (x a b)
也可以使用宏push来加入元素:
(push 'x foo) ;; (x a b)
list函数可以生成一个列表:
(list 1 2 3) ;; (1 2 3)
前面几个例子中, 产生一个列表, 经常要用到quote函数, 直接使用cons或list函数来产生列表, 与使用quote函数来产生列表, 有什么区别?
'((+ 1 2) 3) ;; ((+ 1 2) 3) (list (+ 1 2) 3) ;; (3 3)
可以看出, quote是直接把参数返回, 而不进行求值; 而list是对参数求值后再生成一个列表.
增加元素到列表
在列表前增加元素:
(setq foo '(a b)) ;; (a b) (cons 'x foo) ;; (x a b)
在列表后增加元素:
(append '(a b) '(c)) ;; (a b c)
append的参数也不一定就非要列表, 也可以是其他对象:
(append '(a b) 'c) ;; (a b . c)
对这个结果再使用append函数, 会报错.
append函数还可以将向量转成列表:
(append [a b] "cd" nil) ;; (a b 99 100) ;; nil是必须的, 否则结果如下 (append [a b] "cd") ;; (a b . "cd")
把列表当作数组
对于一个列表, 可以使用car函数取第一个元素, cadr函数取第二个元素, cdr取剩下的元素.
(car '(0 1 2 3 4 5)) ;; 0 (cadr '(0 1 2 3 4 5)) ;; 1 (cdr '(0 1 2 3 4 5)) ;; (1 2 3 4 5)
取第n个元素, 可以使用nth函数:
(nth 3 '(0 1 2 3 4 5)) ;; 3
列表是由链表这种数据结构来实现的, 不适合随机访问, 如果经常要使用这些操作, 还是要用数组更合适
.
修改cons cell的内容
(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)
把列表当堆栈用
后进先出
(setq foo nil) ;; nil (push 'a foo) ;; (a) (push 'b foo) ;; (b a) (pop foo) ;; b
重排列表
(setq foo '(a b c)) ;; (a b c) (reverse foo) ;; (c b a)
sort函数是个破坏性函数, 有可能会在不知不觉间丢失列表元素.
把列表当关联表
关联表(association list)指的是键值对. Elisp中有hash table, 但是hash table有几个缺点:
- hash table里的关键字key是无序的, 而关联表的关键字可以按想要的顺序排列.
- hash table没有列表那样丰富的函数可用.
- hash table没有读入语法和输入形式, 这对于调试和使用都会带来许多不便.
hash table的优点是效率较高.
关联表的键放在CAR中, 对应的数据放在CDR中.
使用assq(对应eq)和assoc(对应equal)两个函数来查询键所对应的值, 再使用cdr来得到对应的数据.
(assoc "a" '(("a" 97) ("b" 98))) ;; ("a" 97) (cdr (assoc "a" '(("a" 97) ("b" 98)))) ;; (97) (assq 'a '((a . 97) (b . 98))) ;; (a . 97) (cdr (assq 'a '((a . 97) (b . 98)))) ;; 97
assoc-default可以一次性完成这样的操作:
(assoc-default "a" '(("a" 97) ("b" 98))) ;; (97)
已知值, 查找对应的键:
(rassoc '(97) '(("a" 97) ("b" 98) )) ;; ("a" 97) (rassq '97 '((a . 97) (b . 98))) ;; (a . 97)
修改关键字对应值的方法:
- 使用cons把新的键值对加到列表的前端. 但是这样会让列表越来越长, 浪费空间.
- 使用setcdr来更改键对应的值, 但是这要先确定键值对在这个列表中, 否则会出错.
- 用assoc查找对应的元素, 再用delq删除该数据, 最后用cons加到列表中.
(setq foo '(("a" . 97) ("b" . 98))) ;; (("a" . 97) ("b" . 98)) ;; 使用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)) ;; 使用assoc, delq, cons来修改 (setq foo (cons '("a" . 97) (delq (assoc "a" foo) foo))) ;; (("a" . 97) ("b" . 98))
推荐使用最后一种, 代码简洁.
遍历列表
使用函数mapc或mapcar来遍历列表. 它们的第一个参数是一个函数, 该函数只接受一个参数, 每次处理列表里的一个元素. 区别是: 前者返回的还是输入的列表, 后者返回的是函数返回值构成的列表.
(mapc '1+ '(1 2 3)) ;; (1 2 3) (mapcar '1+ '(1 2 3)) ;; (2 3 4)
还有一种遍历列表的方法: dolist. 语法结构: (dolist (var list [result]) body...)
var是一个临时变量, 在body里可以用来得到列表中元素的值. 如果不指定返回值, 则返回nil.
(dolist (foo '(1 2 3)) (1+ foo)) ;; nil (setq bar nil) (dolist (foo '(1 2 3) bar) (push (1+ foo) bar)) ;; (4 3 2)
数组和序列
序列=数组+列表 数组=字符串+向量+char table和boolean vector
- 数组的第一个元素下标为0.
- 数组内的元素可以在常数时间内访问.
- 数组在创建后无法改变长度.
- 用aref访问数组, aset设置数组.
向量可以看成是通用的数组, 它的元素是任意对象.
字符串是特殊数组, 它的元素是字符.
测试函数
sequencep: 测试是否为序列 arrayp: 测试是否为数组
序列的通用函数
length: 得到序列长度, 不适用于点列表或环形列表 safe-length: 可以用于点列表和环形列表 elt: 取得序列的第n个元素 nth: 取得列表的第n个元素 aref: 取得数组的第n个元素
数组操作
创建数组, 法一:
(vector 'foo 23 [bar baz] "rats") ;; [foo 23 [bar baz] "rats"]
创建数组, 法二:
foo ;; (a b) [foo] ;; [foo] (vector foo) ;; [(a b)]
make-vector: 生成相同元素的向量 fillarray: 把整个数组用某元素填充
(make-vector 9 'Z) ;; [Z Z Z Z Z Z Z Z Z] (fillarray (make-vector 3 'Z) 5) ;; [5 5 5]
aref和aset可以用于访问和修改数组的元素, 如果使用下标超出数组长度, 则会出错.
vconcat可以把多个序列连成一个向量, 但是这个序列必须是真列表. 这是把列表转换成向量的方法, 向量转列表使用append
(vconcat [A B C] "aa" '(foo (6 7))) ;; [A B C 97 97 foo (6 7)]
符号
符号是有名字的对象, 通过符号, 可以得到和这个符号相关联的信息, 如值, 函数, 属性列表等等.
符号的命名规则: 可包含任何字符, 大多数符号含有字母, 数字和标点(-+=*/). 名字前缀要能把符号名和数字区分开来, 如果需要的话, 可以用\来表示这是一个符号.
(symbolp '+1) ;; nil (symbolp '\+1) ;; t (symbol-name '\+1) ;; "+1"
创建符号
Elisp中会有一个表来保存符号, 这个表称为obarray, 是一个向量.
当Emacs创建一个符号时, 首先会对这个名字求hash值, 得到一个obarray的下标.
当Elisp读入一个符号时, 通常会先查找这个符号是否在obarray中出现过, 没出现则将该符号加入到obarray中, intern函数完成查找并加入的过程. 我们也可以指定一个obarray来装符号.
intern-soft与intern不同的是, 当名字不在obarray中时, intern-soft会返回nil, 而intern会加入到obarray中.
为了不污染obarray, 下面的例子使用名为foo的obarray来保存符号. 如果没有foo这个参数, 则会在obarray中进行, 结果相同.
(setq foo (make-vector 10 0)) ;; [0 0 0 0 0 0 0 0 0 0] (intern-soft "abc" foo) ;; nil foo ;; [0 0 0 0 0 0 0 0 0 0] (intern "abc" foo) ;; abc foo ;; [0 0 0 0 0 0 0 0 0 abc] (intern-soft "abc" foo) ;; abc
Elisp每读入一个符号, 都会intern到obarray中, 如果想避免, 则在符号名前加 #:
(intern-soft "abcd") ;; nil '#:abcd (intern-soft "abcd") ;; nil
(unintern name &optional obarray): 将name从obarray中去除, 成功去除返回t, 没有查到对应的符号则返回nil.
符号的组成
求值规则
一个要求值的lisp对象被称为表达式. 所有的表达式可以分为三种: 符号, 列表和其他类型.
符号表达式的求值: 结果就是符号的值, 如果它没有值则会出错.
列表表达式的求值: 根据第一个元素, 可分为函数调用, 宏调用和特殊表达式三种. 如果第1个元素是函数调用, 则先对列表中其他元素求值, 求值结果作为函数调用的参数. 如果第1个元素是宏对象, 列表里的其他元素不会立即求值, 而是根据宏定义进行扩展. 如果第1个元素是特殊表达式, 则一般用于控制结构或者变量绑定.
变量
Elisp中的变量, 包括全局变量和let绑定的局部变量.
关于let绑定的局部变量, 如果一个变量名既是全局变量也是局部变量, 或者用let多层绑定, 只有最里层的那个变量是有效的.
buffer-local变量
Emacs能使各个缓冲区之间不相互冲突, 很大程度上归功于buffer-local变量.
声明buffer-local变量的方法: make-variable-buffer-local或make-local-variable. 其中, make-variable-buffer-local会在所有缓冲区内都产生一个buffer-local变量, 而make-local-variable则在当前缓冲区内产生一个buffer-local变量. 推荐使用make-local-variable.
(with-current-buffer buffer body)的作用是使唤其中的body表达式在buffer这个缓冲区中执行.
(get-buffer)可以用缓冲区的名字得到对应的缓冲区对象, 如果没有这样的名字, 则返回nil.
使用buffer-local的例子
(setq foo "I'm global variable") ;; "I'm global variable" (make-local-variable 'foo) ;; foo foo ;; "I'm global variable" (setq foo "I'm buffer-local variable") ;; "I'm buffer-local variable" foo ;; "I'm buffer-local variable" (with-current-buffer "*Messages*" foo) ;; "I'm global variable"
可见, 如果一个值在作为全局变量时有一个值, 使用make-local-variable将变量声明为buffer-local变量后, 对其进行的改变, 只能在当前缓冲区中生效, 而在其他缓冲区则仍使用其作为全局变量时的值. 其在作为全局变量时的值, 叫做默认值, 可以用default-value来查看.
(default-value 'foo) ;; "I'm global variable"
而要修改全局变量的默认值, 可以使用setq-default来修改.
local-variable-p: 测试是否为buffer-local buffer-local-value: 在当前缓冲区内得到其他缓冲区的buffer-local变量. 如: (buffer-local-value 'foo (get-buffer "scratch"))
变量的作用域
函数和命令
参数
在Elisp中, 参数分为必须的, 可选的, 剩余的. 格式为: (required-vars ... &optional optional-vars ... &rest rest-var)
如:
(defun foo (var1 var2 &optional opt1 opt2 &rest rest) (list var1 var2 opt1 opt2 rest)) (foo 1 2) => (1 2 nil nil nil) (foo 1 2 3) => (1 2 3 nil nil) (foo 1 2 3 4 5 6) => (1 2 3 4 (5 6))
文档
给函数提供一个文档说明是比较好的习惯.
(defun foo (var1) "test" (list var1))
宏
与函数类似. 但宏的参数是出现在最后扩展后的表达式中, 而函数参数是求值后才传递给这个函数. 如:
Generated by Emacs 25.x(Org mode 8.x)
Copyright © 2014 - Pinvon - Powered by EGO