Pinvon's Blog

所见, 所闻, 所思, 所想

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中使用 letlet* 来对局部变量进行绑定.

(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有几个缺点:

  1. hash table里的关键字key是无序的, 而关联表的关键字可以按想要的顺序排列.
  2. hash table没有列表那样丰富的函数可用.
  3. 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)

修改关键字对应值的方法:

  1. 使用cons把新的键值对加到列表的前端. 但是这样会让列表越来越长, 浪费空间.
  2. 使用setcdr来更改键对应的值, 但是这要先确定键值对在这个列表中, 否则会出错.
  3. 用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

  1. 数组的第一个元素下标为0.
  2. 数组内的元素可以在常数时间内访问.
  3. 数组在创建后无法改变长度.
  4. 用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))

与函数类似. 但宏的参数是出现在最后扩展后的表达式中, 而函数参数是求值后才传递给这个函数. 如:


Comments

使用 Disqus 评论
comments powered by Disqus