我在淘宝买了一个 《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中的术 语是“求值”),计算机将完成三件事情:

  1. 返回列表本身
  2. 告诉你一个出错信息
  3. 将列表的第一个符号当做一个命令,然后执行这个命令

引用 (')

单引号('),表示一个引用。单引号出现在一个列表前,告诉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

简单直观的编程思路可以这样:

  1. 定义含100个整数的数组(假设使用的程序语言数组基数从0开始计算),初 始值为0,数组的第j-1个元素的值表示第j盏灯的状态
  2. 外循环为人(1-100),第i个人 ( 0<i<101)
  3. 第i个人的内循环处理各盏灯。第j (0<j<101)盏灯的时候这样处理:如果 j 除以 i 除尽,就把 数组第 j-1 个元素加一。
  4. 最终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。这两个函数区别在于:

  1. % 的第一个参数必须是整数,而 mod 的第一个参数可以是整数也可以是浮 点数。
  2. 即使对相同的参数,两个函数也不一定有相同的返回值:

三角运算

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)。