我在淘宝买了一个 《Elisp 编程入门》,在 Emacs 的 info 里也有其英文版 本,info 里当然还有 Elisp 参考手册。
第零步
这里是第零步,假设你已经知道 "按下 C-j" "按下 C-u C-x C-e" 是什么意思, 这就足够。
参考了资料3,用一个 "Hello World" 开始,说明如何实验我们 elisp 语句。
首先切换到 scratch 缓冲区里。如果这个缓冲区的当前主模式不是 lisp-interaction-mode,请用 M-x lisp-interaction-mode 命令先转换到 lisp-interaction-mode (使用这个模式,我们可以使用 C-j 键运行 elisp 语 句,当然在 Emacs 环境中,任何模式下,任何缓冲区里都可以执行 elisp 语句 的)。先输入:
(message "Hello world !")
如果你已经在 "lisp-interaction-mode" 模式下,那么将光标移到行尾右括号 后,按 C-j 键。我的 Emacs 23 版本里,"Hello world !" 直接显示在下一行, 同时在 Emacs 窗口最底下的 Minibuffer 里也有显示:
(message "Hello world !")
"Hello world !"
如果你不在 "lisp-interaction-mode" 模式下,在任何缓冲区都可以,同样将光 标移到这一句行尾。按下 "C-x C-e" 键,Minibuffer 里将显示 "Hello world !"。如果想输出到缓冲区中,使用 "C-u C-x C-e" 键序列。我的 Emacs 23 按下 "C-u C-x C-e" 是这样:
(message "Hello world !")"Hello world !"
请确保这些非常简单的操作在你的 Emacs 里实验成功。这样才方便进入下一步。
简单概念
列表
Lisp 中的一个列表(任何列表)都是一个准备运行的程序。运行它(Lisp中的术 语是“求值”),计算机将完成三件事情:
- 返回列表本身
- 告诉你一个出错信息
- 将列表的第一个符号当做一个命令,然后执行这个命令
引用 (')
单引号('),表示一个引用。单引号出现在一个列表前,告诉Lisp不要对这个列表 做任何操作,仅仅保持其原样。如果一个列表前没有引号,这个列表的第一个符 号就是计算机将要执行的一条命令(Lisp中,这些命令称为函数)。
'(setq 次数 20) ; 光标定位到这个列表尾部,按 C-x C-e 得到列表本身
(setq 次数 10) ; 光标定位到它的尾部, C-x C-e 得到 10
(if (< 次数 10)
(message "次数是 %d,小于 10" 次数)
(message "次数是 %d, 大于 10" 次数)) ; C-x C-e 得到“大于10”的提示。
上例中 '(setq 次数 20) 就是一个引用,对它执行计算(C-x C-e)得到列表本身, (setq 次数 10) 中的 setq 是此列表的第一个符号(即Lisp中的函数),执行它 回显区得到10(真正的情况是,这个列表返回值为10,重要的是副作用 — 将 “次数”的值设置为10)。再接下来的 if , < , message 都是Lisp命令(函 数)。
变量
全局变量
elisp 函数都是全局的,用 defvar 和 setq 定义的变量都是全局变量。
使用 defvar 定義變量
(defvar 变量名 变量值
"变量描述文档")
示例:
(defvar 文章 '石碏諫寵州籲
"取自古文觀止")
在上面括號後面按 "C-x C-e" 安裝這個變量定義,然後就可以用 "C-h v" 得到 變量 "文章" 的描述了:
文章's value is 石碏諫寵州籲
Documentation:
取自古文觀止
[back]
使用 set 和 setq 定义变量
(set '文章甲 '石碏諫寵州籲)
现在可以用 "C-h v" 查看变量 "文章甲" 的"描述"(没有描述,只有值)。
注意,"文章甲" 因为开始没有定义,所以使用 set 的时候要加单引号引用。为 了方便的定义变量,这就出现了 setq 。
(setq 文章乙 '石碏諫寵州籲)
所以,我们看到的基本都是 setq 而不是 set 。
局部作用域变量
都用全局变量容易引起冲突(覆盖有用变量),所以在函数内通常用 let 定义局 部作用域的变量。比如下面的 let 定义了 "圆周率" 为 3.1415926 , "面积" 为空(nil):
(defun 圆面积 (半径)
"用给定的参数做半径,计算圆面积。"
(let ((圆周率 3.1415926)
面积)
(setq 面积 (* 圆周率 半径 半径))
(message "半径为 %.2f 的圆面积是 %.2f" 半径 面积)))
(圆面积 3)
执行后在 Minibuffer 可以看到:
半径为 3.00 的圆面积是 28.27"
另外,上面的函数 "圆面积" 安装成功后可以用 "C-h f" 查看描述:
圆面积 is a Lisp function.
(圆面积 半径)
用给定的参数做半径,计算圆面积。
[back]
是不是很明白?第一,说明 Elisp 里函数基本都是全局作用域;第二,可以看到 用中文多简单明了!
注:现在用 "C-h v" 是查不到 "圆周率" 和 "面积" 变量的。
lambda 函数
相当于一个匿名函数,Python里的 lambda 函数就是从 lisp 里学习的。
形式:
(lambda (参数列表)
"函数描述文档"
函数体)
调用(同时也是定义) lambda 方法如下:
(funcall (lambda (姓名) "向给定人问好!"
(message "你好,%s!" 姓名)) "jianlee")
也可以把 lambda 定义赋值给一个变量,再用 funcall 调用(不过,这就没必要 用 lambda 了,也许你要认真定义一个函数):
(setq foo (lambda (姓名) "向给定人问好!"
(message "你好,%s!" 姓名)))
(funcall foo "jianlee")
控制结构
每个程序语言都有控制结构,以便处理各种情况。
顺序执行
一般来说程序都是按表达式顺序依次执行的。这在 defun 等特殊环境中是自动进 行的。但是一般情况下都不是这样的。比如你无法用 eval-last-sexp 同时执行 两个表达式,在 if 表达式中的条件为真时执行的部分也只能运行一个表达式。 这时就需要用 progn 这个特殊表达式。它的使用形式如下:
(progn A B C ...)
它的作用就是让表达式 A, B, C 顺序执行。比如:
(progn
(setq foo 3)
(message "Square of %d is %d" foo (* foo foo)))
progn 相当于一个程序块。在 shell 中我们用 () 或 {} 来让一批命令一块执行。
条件语句
elisp 有两个最基本的条件判断表达式 if 和 cond。if 相当于其他语言的 if , cond 相当于 case。使用形式分别如下:
(if 条件判断
then部分
else部分)
(cond (case1 do-when-case1)
(case2 do-when-case2)
...
(t 其他情况执行块))
if (可以不要 else ) 示例 :
(defun 谁大 (甲 乙)
"比较给定的两个数谁大。"
(if (> 甲 乙)
(message "%d 更大!" 甲)
(message "%d 更大!" 乙)))
(谁大 5 9)
cond 示例:
(defun 说一句 (给一字)
"通过 '给一字' 变量的值不同,输出一句诗。"
(let ((么 给一字)); 将 "给一字" 给 "么"
(cond ((string= 么 "首") (message "%s: 鹤立松梢月" 么))
((string= 么 "颔") (message "%s: 鱼行水底天" 么))
((string= 么 "颈") (message "%s: 风光都占尽" 么))
((string= 么 "尾") (message "%s: 不费一文钱" 么))
(t (message "你输入的不是东西: %s" 给一字)))))
(说一句 "颔")
(说一句 "尾")
(说一句 "無")
安装好 "说一句" 函数,用 "C-h f" 可以看到“详细”文档。之所以用 let 定义 一个局域变量 "么" , 完全是偷懒。定义函数的时候我们尽量让变量名有意义 (可以就长点),内部使用的时候可以绑定到一个简单的局域变量上。
注意: let 定义的 "么" 作用域为 let 列表括号包围的范围。
说明: "string=" 是 "string-equal" 别名。相关比较还有
= |
数值比较 |
eq |
对象是否相同 |
equal |
对象是否相同 |
char-equal |
字符比较 |
string-equal |
字符串比较 |
string< |
同上 |
string-lessp |
string< 的别名 |
参考3有个显示斐波那契数列示例:
(defun fib (n)
(cond ((= n 0) 0)
((= n 1) 1)
(t (+ (fib (- n 1))
(fib (- n 2))))))
(fib 10) ; => 55
试试把 10 换成 100 ,如果机器不好,这个递归算法会算死你。
循环
我们写个函数计算高斯在小学就会做的例子:
(defun 高斯 (玄)
"计算从 1 到 '玄' 之间所有整数之和,包括这两个数。"
(let ((总和 1)
(保存玄 玄))
(while (> 玄 1)
(setq 总和 (+ 总和 玄)
玄 (- 玄 1)))
(message "从 1 加到 %d 值为: %d" 保存玄 总和)))
(高斯 100)
参考3还有一个阶乘的例子:
(defun factorial (n)
(let ((res 1))
(while (> n 1)
(setq res (* res n)
n (- n 1)))
res))
(factorial 10)
逻辑运算
逻辑运算和其它语言都是很类似的,使用 and、or、not。and 和 or 也同样具有 短路性质。很多人喜欢在表达式短时,用 and 代替 when,or 代替 unless。当 然这时一般不关心它们的返回值,而是在于表达式其它子句的副作用。比如 or 经常用于设置函数的缺省值,而 and 常用于参数检查:
(defun hello-world (&optional name)
(or name (setq name "Emacser"))
(message "Hello, %s" name)) ; => hello-world
(hello-world) ; => "Hello, Emacser"
(hello-world "Ye") ; => "Hello, Ye"
下面是参考3中的示例(原示例有误,不能准确判断平方数):
(defun square-number-p (n)
(and (>= n 0)
(= (/ n (round (sqrt n))) (sqrt n))))
(square-number-p -1) ; => nil
(square-number-p 25) ; => t
下面写一个函数 “平方数” 输出小于等于给定整数的所有平方数(利用上面定义 的 square-number-p 函数判断一个数是否为平方数):
(defun 平方数 (玄)
"输出小于 '玄' 的所有平方数"
(setq 玄 (+ 玄 1))
(let ((变 1)
序列)
(while (< 变 玄)
(and (square-number-p 变)
(setq 序列 (concat 序列 " " (int-to-string 变))))
(setq 变 (+ 变 1)))
(message "数列: %s" 序列)))
(平方数 100)
int-to-string
将整数转变为字符串,一些程序语言自动转换,不自动转换一 定有这么一个函数。
concat
连接字符串
要实现输出100以内所有平方数,还有一个很简单的方法,从 1 开始计算平方 和,如果平方和小于100就终止。
说明: "square number" 是平方数,参见 http://zh.wikipedia.org/wiki/平方数 。 square-number-p 函数就是测试一个数是否为平方数。
引申一下:关于平方数的一个有趣问题
有个问题:“有100个人和100盏灯,并分别编号,灯开始都是灭的,从第一个人开 始拉灯的开关,第i个人只拉编号为i的倍数的灯(比如第3个人只拉第3、6、 9、...、99盏灯),求100个人都拉完相应的灯后,亮着的灯的编号是哪些?”
这和求100以内平方数是相似的,最终亮着的灯都是100以内平方数:
1 4 9 16 25 36 49 64 81 100
简单直观的编程思路可以这样:
- 定义含100个整数的数组(假设使用的程序语言数组基数从0开始计算),初 始值为0,数组的第j-1个元素的值表示第j盏灯的状态
- 外循环为人(1-100),第i个人 ( 0<i<101)
- 第i个人的内循环处理各盏灯。第j (0<j<101)盏灯的时候这样处理:如果 j 除以 i 除尽,就把 数组第 j-1 个元素加一。
- 最终100个数组中值为奇数的元素表示灯亮。
基本数据类型
数字
测试函数
(integerp 1.) ; => t
(integerp 1.0) ; => nil
(floatp 1.) ; => nil
(floatp -0.0e+NaN) ; => t
(numberp 1) ; => t
还有:
zerop |
是否为零 |
wholenump |
是否为非负数 |
大小比较
同样有 <, >, >=, <=。不一样的是,赋值是使用 set 函 数, = 不再是一个赋值运算符了,而是测试数字相等符号。 和其它语言类似,对于浮点数的相等测试都是不可靠的,这需要在一个范围内比 较。
测试数字是否相等的函数 eql,这是函数不仅测试数字的值是否相等,还测试数 字类型是否一致:
(= 1.0 1) ; => t
(eql 1.0 1) ; => nil
数的转换
整数向浮点数转换是通过 float 函数进行的。而浮点数转换成整数有这样几个函 数:
truncate |
转换成靠近 0 的整数 |
floor |
转换成最接近的不比本身大的整数 |
ceiling |
转换成最接近的不比本身小的整数 |
round |
四舍五入后的整数,换句话说和它的差绝对值最小的整数 |
(truncate 10.909) ; ==> 10
(floor 10.909) ; ==> 10 (比 10.909 小的最大整数)
(ceiling 10.009) ; ==> 11 (比 10.009 大的最小整数)
(round 10.909) ; ==> 11
(round 10.009) ; ==> 10
四则运算
(setq foo 10) ; => 10
(setq foo (1+ foo)) ; => 11
(setq foo (1- foo)) ; => 10
abs 取数的绝对值。
有两个取整的函数,一个是符号 %,一个是函数 mod。这两个函数区别在于:
- % 的第一个参数必须是整数,而 mod 的第一个参数可以是整数也可以是浮 点数。
- 即使对相同的参数,两个函数也不一定有相同的返回值:
三角运算 |
sin, cos, tan, asin, acos, atan。 |
开方函数 |
sqrt |
exp 是以 e 为底的指数运算,expt 可以指定底数的指数运算。log 默认底数是 e,但是也可以指定底数。log10 就是 (log x 10)。logb 是以 2 为底数运算, 但是返回的是一个整数。这个函数是用来计算数的位。
random 可以产生随机数。可以用 (random t) 来产生一个新种子。虽然 emacs 每次启动后调用 random 总是产生相同的随机数,但是运行过程中,你不知道调 用了多少次,所以使用时还是不需要再调用一次 (random t) 来产生新的种子。
字符串
在 emacs 里字符串是有序的字符数组。和 c 语言的字符串数组不同,emacs 的 字符串可以容纳任何字符,包括 \0:
(setq foo "abc\000abc") ; => "abc^@abc"
构成字符串的字符其实就是一个整数。一个字符 'A' 就是一个整数 65。但是目 前字符串中的字符被限制在 0-524287 之间(未考证)。字符的读入语法是在字 符前加上一个问号,比如 ?A 代表字符 'A'。
?A ; => 65
?a ; => 97
(logior (lsh 1 27) ?A)
注:直接在 "?A" 之后按 "C-x C-e" 在 Minibuffer 就得到 65。
子串
(substring "0123456789" 3) ; => "3456789"
(substring "0123456789" 3 5) ; => "34"
(substring "0123456789" -3 -1) ; => "78"
转换
数字和字符串之间的转换可以用 number-to-string 和 string-to-number。其中 string-to-number 可以设置字符串的进制,可以从 2 到 16。 number-to-string 只能转换成 10 进制的数字。如果要输出八进制或者十六进 制,可以用 format 函数:
(string-to-number "256") ; => 256
(number-to-string 256) ; => "256"
(format "%#o" 256) ; => "0400"
(format "%#x" 256) ; => "0x100"
concat 可以把一个字符构成的列表或者向量转换成字符串,vconcat 可以把一个 字符串转换成一个向量,append 可以把一个字符串转换成一个列表。
(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 只使第一个单词的第一个字符大写,其它字符大 小写不变。这两个函数的参数如果是一个字符,那么只让这个字符大写。
原笔记
缓冲区
缓冲区是 Emacs 工作的舞台。
buffer-name ; 得到缓冲区名字
buffer-file-name ; 得到缓冲区关联的文件名
current-buffer ; 得到缓冲区本身
other-buffer ; 最近访问的缓冲区
相关函数
(switch-to-buffer (other-buffer))
(set-buffer (other-buffer))
(buffer-size)
(point)
(point-min)
(point-max)
set-buffer 将计算机的注意力切换到另外一个缓冲区,屏幕上的缓冲区不改变。
函数
defun (定义函数)
(defun 乘7 (number)
"一个数乘以7"
(* 7 number))
(乘7 7)
可以通过把光标移到函数的结尾括号后按 C-x C-e 安装函数。再把光标移到下面一 行的结尾括号后按同样的键计算表达式结果。
查看刚才的函数定义详情,可以用 C-h f ,输入函数名"乘7",就可以看到emacs打 开一个buffer显示“一个数乘以7”了。
使函数可以交互
(defun 乘7 (number)
"一个数乘以7"
(interactive "p")
(message "计算结果是 %d" (* 7 number)))
这样我们输入 C-u 9 M-x 乘7 之后,可以看到“计算结果是 63”显示在minibuffer。 其中"p"是让emacs传递一个前缀参量给此函数。
其他 interactive 选项:
r -- Region: point and mark as 2 numeric args, smallest first. Does no I/O.
; 将位点所在区域的位点值与标记值作为函数的两个参量
let 函数
let 表达式由三部分组成:
- 第一部分:let 符号
- 第二部分:变量列表(varlist),列表中每一个元素是一个符号或者一个两元素 的列表,而它的第一个元素一定是一个符号
- 第三部分:let表达式主体
(let ((境界一 '昨夜西风调碧树,独上高楼,望断天涯路)
(境界二 '衣袋渐宽终不悔,为伊销得人憔悴))
(message "古人求知,境界有三:\n境界一 --- %s\n境界二 --- %s"
境界一 境界二))
上面定义了两个变量:境界一,境界二。在函数定义结尾括号后按 "C-u C-x C-e" 函数的结果就显示在buffer里面。不加 "C-u" 就显示在 minibuffer 中。
如果变量没有绑定到一个特定值上,默认是 nil:
(let ((境界一 '昨夜西风调碧树,独上高楼,望断天涯路)
(境界二 '衣袋渐宽终不悔,为伊销得人憔悴)
(境界三))
(message "古人求知,境界有三:\n境界一 --- %s\n境界二 --- %s\n境界三 --- %s"
境界一 境界二 境界三))
可以用数字:
(let ((方法几何 3)
(境界一 '昨夜西风调碧树,独上高楼,望断天涯路)
(境界二 '衣袋渐宽终不悔,为伊销得人憔悴)
(境界三))
(message "古人求知,境界有%d:\n境界一 --- %s\n境界二 --- %s\n境界三 --- %s"
方法几何 境界一 境界二 境界三))
求值得:
"古人求知,境界有3:
境界一 --- 昨夜西风调碧树,独上高楼,望断天涯路
境界二 --- 衣袋渐宽终不悔,为伊销得人憔悴
境界三 --- nil"
if 特殊表
一个简单的if片断,其意自见:
(if (> 5 4)
(message "5大于4!"))
函数中使用if:
(defun 释义 (名字)
"简单输出一个名词的解释"
(if (equal 名字 'GNU)
(message "GNU's Not Unix!")))
安装这个函数后,计算 "(释义 'GNU)" 就会得到 "GNU's Not Unix!" 输出。
if-then-else
if 函数的可以有第三部分:
(defun 释义 (名字)
"简单输出一个名词的解释"
(if (equal 名字 'GNU)
(message "GNU's Not Unix!")
(message "抱歉,现在还只能解释GNU ^_^")))
现在计算 "(释义 '任何字符串)" ,如果"任何字符串"不是"GNU",那么输出"抱 歉,现在还只能解释GNU ^_^"
Lisp 的真假
lisp中出来 nil,其他都为真。nil有两种意思,一种是空列表,一种是nil。可以 将nil写作 ()或nil。
;; 4是真,所以下面函数返回 true
(if 4
'true
'false)
;; nil 是假,所以下面函数返回 false
(if nil
'true
'false)
;; () 是 nil的另外一种写法
(if ()
'true
'false)
save-excursion 函数
这个函数和 emacs 的很多关键概念关系密切。先解释一下 emacs 中的几个概念:
位点(point)
光标当前所处的位置。从1开始到最后。
标记(mark)
缓冲区的另外一个位置。可以用 set-mark-command 设置(通常绑 定在 C-spc 或 C-z 或 C-@)。设置了一个mark后,可以使用 exchange-point-and-mark 在point和mark两点间来回跳转。
region
位点和标记之间的缓冲区称为region,也称域,区域,现域。很多命令 都是对域操作:center-region,count-lines-region,kill-region,print-region
save-excursion 函数将位点和标记的当前位置保存起来,以便返回。
(save-excursion
body ...)
或
(let varlist
(save-excursion
body ...))
与缓冲区有关的几个函数
简化的 beginning-of-buffer
(defun 跳到开始 () ;; 没有参数,就用空列表
"简单的调到缓冲区的开始" ;; 注释
(interactive) ;; 不需要变量,可以交互
(push-mark) ;; 在光标当前位置设置一个mark,并保存起来
(goto-char (point-min))) ;; 跳到开始
安装上面的函数,用 "M-x 跳到开始" 可以将光标跳到缓冲区开始。
mark-whole-buffer 函数
(defun mark-whole-buffer ()
"Put point at beginning and mark at end of buffer."
(interactive)
(push-mark (point))
(push-mark (point-max))
(goto-char (point-min)))
错误消息分析
Emacs Lisp 错误消息有些独特,一开始不容易读懂。
Symbol's function definition is void: ...
比如我们有下面一个列表:
(这是 一个 测试)
执行它,在 "XBacktraceX" (X代表*号) 中会出现下面错误提示:
Debugger entered--Lisp error: (void-function ...)
省略号是中文, void-function 表示 “这是” 不是一个函数。正因为 “这是” 不 是一个函数,才导致列表“没有函数”(void-function)。