Elisp 函数和命令
在 elisp 里类似函数的对象很多,比如:
- 函数。这里的函数特指用 lisp 写的函数。
- 原子函数(primitive)。用 C 写的函数,比如 car、append。
- lambda 表达式
- 特殊表达式
- 宏(macro)。宏是用 lisp 写的一种结构,它可以把一种 lisp 表达式转换成等价的另一个表达式。
- 命令。命令能用 command-execute 调用。函数也可以是命令。
以上这些用 functionp 来测试都会返回 t。
我们已经学过如何定义一个函数。但是这些函数的参数个数都是确定。但是你可以看到 emacs 里有很多函数是接受可选参数,比如 random 函数。还有一些函数可以接受不确定的参数,比如加减乘除。这样的函数在 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))
从这个例子可以看出,当可选参数没有提供时,在函数体里,对应的参数值都是 nil。同样调用函数时没有提供剩余参数时,其值也为 nil,但是一旦提供了剩余参数,则所有参数是以列表的形式放在对应变量里。
调用函数
通常函数的调用都是用 eval 进行的,但是有时需要在运行时才决定使用什么函数,这时就需要用 funcall 和 apply 两个函数了。这两个函数都是把其余的参数作为函数的参数进行调用。那这两个函数有什么参数呢?唯一的区别就在于 funcall 是直接把参数传递给函数,而 apply 的最后一个参数是一个列表,传入函数的参数把列表进行一次平铺后再传给函数,看下面这个例子就明白了
(funcall 'list 'x '(y) '(z)) ; => (x (y) (z)) (apply 'list 'x '(y ) '(z)) ; => (x (y) z)
命令
emacs 运行时就是处于一个命令循环中,不断从用户那得到按键序列,然后调用对应命令来执行。lisp 编写的命令都含有一个 interactive 表达式。这个表达式指明了这个命令的参数。比如下面这个命令
(defun hello-world (name) (interactive "sWhat you name? ") (message "Hello, %s" name))
现在可以用 M-x 来调用这个命令。让我们来看看 interactive 的参数是什么意思。这个字符串的第一个字符(也称为代码字符)代表参数的类型,比如这里 s 代表参数的类型是一个字符串,而其后的字符串是用来提示的字符串。如果这个命令有多个参数,可以在这个提示字符串后使用换行符分开,比如:
(defun hello-world (name time) (interactive "sWhat you name? \nnWhat the time? ") (message "Good %s, %s" (cond ((< time 13) "morning") ((< time 19) "afternoon") (t "evening")) name))
interactive 可以使用的代码字符很多,虽然有一定的规则,比如字符串用 s,数字用 n,文件用 f,区域用 r,但是还是很容易忘记,用的时候看 interactive 函数的文档还是很有必要的。但是不是所有时候都参数类型都能使用代码字符,而且一个好的命令,应该尽可能的让提供默认参数以让用户少花时间在输入参数上,这时,就有可能要自己定制参数。
首先学习和代码字符等价的几个函数。s 对应的函数是 read-string。比如
(read-string "What your name? " user-full-name)
n 对应的函数是 read-number,文件对应 read-file-name。很容易记对吧。其实大部分代码字符都是有这样对应的函数或替换的方法(见下表)。
代码字符 | 代替的表达式 |
---|---|
a | (completing-read prompt obarray ’fboundp t) |
b | (read-buffer prompt nil t) |
B | (read-buffer prompt) |
c | (read-char prompt) |
C | (read-command prompt) |
d | (point) |
D | (read-directory-name prompt) |
e | (read-event) |
f | (read-file-name prompt nil nil t) |
F | (read-file-name prompt) |
G | 暂时不知道和 f 的差别 |
k | (read-key-sequence prompt) |
K | (read-key-sequence prompt nil t) |
m | (mark) |
n | (read-number prompt) |
N | (if current-prefix-arg (prefix-numeric-value current-prefix-arg) (read-number prompt)) |
p | (prefix-numeric-value current-prefix-arg) |
P | current-prefix-arg |
r | (region-beginning) (region-end) |
s | (read-string prompt) |
S | (completing-read prompt obarray nil t) |
v | (read-variable prompt) |
x | (read-from-minibuffer prompt nil nil t) |
X | (eval (read-from-minibuffer prompt nil nil t)) |
z | (read-coding-system prompt) |
Z | (and current-prefix-arg (read-coding-system prompt)) |
知道这些表达式如何用于 interactive 表达式里呢?简而言之,如果 interactive 的参数是一个表达式,则这个表达式求值后的列表元素对应于这个命令的参数。请看这个例子:
(defun read-hiden-file (file arg) (interactive (list (read-file-name "Choose a hiden file: " "~/" nil nil nil (lambda (name) (string-match "^\\." (file-name-nondirectory name)))) current-prefix-arg)) (message "%s, %S" file arg))
第一个参数是读入一个以 “.” 开头的文件名,第二个参数为当前的前缀参数(prefix argument),它可以用 C-u 或 C-u 加数字提供。list 把这两个参数构成一个列表。这就是命令一般的自定义设定参数的方法。
需要注意的是 current-prefix-arg 这个变量。这个变量当一个命令被调用,它就被赋与一个值,你可以用 C-u 就能改变它的值。在命令运行过程中,它的值始终都存在。即使你的命令不用参数,你也可以访问它
(defun foo () (interactive) (message "%S" current-prefix-arg))
用 C-u foo 调用它,你可以发现它的值是 (4)。那为什么大多数命令还单独为它设置一个参数呢?这是因为命令不仅是用户可以调用,很可能其它函数也可以调用,单独设置一个参数可以方便的用参数传递的方法调用这个命令。事实上所有的命令都可以不带参数,而使用前面介绍的方法在命令定义的部分读入需要的参数,但是为了提高命令的可重用性和代码的可读性,还是把参数分离到 interactive 表达式里好。