基础知识
这一节介绍一下 elisp 编程中一些最基本的概念,比如如何 定义函数 ,程序的 控制结构 , 变量 的使用和 作用域 等等
函数
elisp 中定义一个函数是用这样的形式:
(defun function-name (arguments-list) "document string" body)
比如:
(defun hello-world (name) "Say hello to user whose name is NAME." (message "Hello, %s" name))
其中函数的文档字符串是可以省略的。但是建议为你的函数(除了最简单,不作为接口的)都加上文档字符串 这样将来别人使用你的扩展或者别人阅读你的代码或者自己进行维护都提供很大的方便
在 emacs 里,当光标处于一个函数名上时,可以用 C-h f 查看这个函数的文档。比如前面这个函数,在 Help 缓冲区里的文档是:
hello-world is a Lisp function. (hello-world name) Say hello to user whose name is name.
如果你的函数是在文件中定义的。这个文档里还会给出一个链接能跳到定义的地方
调用函数
要运行一个函数,最一般的方式是:
(function-name arguments-list)
比如前面这个函数:
(hello-world "Emacser") ; => "Hello, Emacser"
每个函数都有一个返回值 。这个返回值一般是函数定义里的 最后一个表达式的值
变量
elisp 里的变量使用无需象 C 语言那样需要声明,可以用 setq 直接对一个变量 赋值 :
(setq foo "I'm foo") ; => "I'm foo" (message foo) ; => "I'm foo"
和函数一样,你可以用 C-h v 查看一个变量的文档。比如当光标在 foo 上时用 C-h v 时,文档是这样的:
foo's value is "I'm foo" Documentation: Not documented as a variable.
有一个特殊表达式 defvar ,它可以 声明 一个变量,一般的形式是:
(defvar variable-name value "document string")
它与 setq 所不同的是:
- 如果变量在声明之前,这个变量已经有一个值的话,用 defvar 声明的变量值不会改变成声明的那个值
- defvar 可以为变量提供文档字符串,当变量是在文件中定义的话,C-h v 后能给出变量定义的位置
(defvar foo "Did I have a value?" "A demo variable") ; => foo foo ; => "I'm foo" (defvar bar "I'm bar" "A demo variable named \"bar\"") ; => bar bar ; => "I'm bar"
用 C-h v 查看 foo 的文档,可以看到它已经变成:
foo's value is "I'm foo" Documentation: A demo variable
由于 elisp 中函数是全局的,变量也很容易成为全局变量(因为全局变量和局部变量的赋值都是使用 setq 函数),名字不互相冲突是很关键的
所以除了为你的函数和变量选择一个合适的前缀之外,用 C-h f 和 C-h v 查看一下函数名和变量名有没有已经被使用过是很关键的
局部作用域的变量
如果没有局部作用域的变量,都使用全局变量,函数会相当难写。elisp 里可以用 let 和 let* 进行局部变量的绑定。let 使用的形式是:
(let (bindings) body)
bingdings 可以是 (var value) 这样对 var 赋初始值的形式,或者用 var 声明一个 初始值为 nil 的变量。比如:
(defun circle-area (radix) (let ((pi 3.1415926) area) (setq area (* pi radix radix)) (message "直径为 %.2f 的圆面积是 %.2f" radix area))) (circle-area 3) ;; =>直径为 3.00 的圆面积是 28.27
C-h v 查看 area 和 local-pi 应该没有这两个变量
let* 和 let 的使用形式完全相同,唯一的区别是在 let* 声明中就 能使用前面声明的变量 ,比如:
(defun circle-area (radix) (let* ((local-pi 3.1415926) (area (* local-pi radix radix))) (message "直径为 %.2f 的圆面积是 %.2f" radix area)))
lambda 表达式
lambda 表达式相当于其它语言中的匿名函数。比如 perl 里的匿名函数。它的形式和 defun 是完全一样的:
(lambda (arguments-list) "documentation string" body)
调用 lambda 方法如下:
(funcall (lambda (name) (message "Hello, %s!" name)) "Emacser")
也可以把 lambda 表达式 赋值 给一个变量,然后用 funcall 调用:
(setq foo (lambda (name) (message "Hello, %s!" name))) (funcall foo "Emacser") ; => "Hello, Emacser!"
lambda 表达式最常用的是 作为参数传递给其它函数 ,比如 mapc
控制结构
顺序执行
一般来说程序都是按表达式顺序依次执行的。这在 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)))
条件判断
elisp 有两个最基本的条件判断表达式 if 和 cond 。使用形式分别如下:
(if condition then else) (cond (case1 do-when-case1) (case2 do-when-case2) ... (t do-when-none-meet))
例子如下:
(defun my-max (a b) (if (> a b) a b)) (my-max 3 4) ; => 4 (defun fib (n) (cond ((= n 0) 0) ((= n 1) 1) (t (+ (fib (- n 1)) (fib (- n 2)))))) (fib 10) ; => 55
还有两个 宏 when 和 unless ,。使用这两个宏的好处是使代码可读性提高:
- when 能省去 if 里的 progn 结构
- unless 省去 条件为真子句需要的的 nil 表达式
循环
循环最常用的是 while 表达式。它的形式是:
(while condition body)
比如:
(defun factorial (n) (let ((res 1)) (while (> n 1) (setq res (* res n) n (- n 1))) res)) (factorial 10)
函数式语言里并不内置循环,实际上是通过递归来实现 while 和 loop 一般都是宏表达式
逻辑运算
条件的逻辑运算和其它语言都是很类似的,使用 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" (defun square-number-p (n) (and (>= n 0) (= (/ n (sqrt n)) (sqrt n)))) (square-number-p -1) ; => nil (square-number-p 25) ; => t