Emacs折腾日记(十三)——函数、宏以及命令
之前在开篇介绍简单的elisp时候就提到过函数,后面的一些示例中也用到了一些函数,但是都是一些基本的概念,这篇将深入了解函数的一些特性。
首先要判断一个符号是否是函数,可以使用 functionp 来判断。
(defun foo()
1)
(foo)
(functionp 'foo) ;; ==> t
(setq var 1)
(functionp 'var) ;; ==> nil
不光函数,以下几种functionp也返回t
- 函数。这里的函数特指用 lisp 写的函数。
- 原子函数(primitive)。用 C 写的函数,比如 car、append。
- lambda 表达式
- 特殊表达式
- 宏(macro)。宏是用 lisp 写的一种结构,它可以把一种 lisp 表达式转换成等价的另一个表达式。
- 命令。命令能用 command-execute 调用。函数也可以是命令。
参数列表的语法
过去我们的所有函数都是定参的函数,也就是说是确定了参数个数的函数。但是实际使用中会大量使用不定参函数,也就是参数不确定的函数。在C/C++ 以及 Python中会大量使用。它的一个使用场景就是某些时候不传就采用默认值,否则就采用用户定义的值。另一个场景就是像printf这样事先无法确定到底要输出多少内容。
elisp中的函数完整定义如下
(defun func (REQUIRED-VARS...
[&optional OPTIONAL-VARS...]
[&rest REST-VAR]))
前面是确定的参数列表,也就是说前面的参数在调用函数时必须传入,而&optional 之后是可选参数,如果要传入可选参数这个 &optional
关键字是必须写上的。这里的可选参数也是需要在定义时一个个的指定出来,但是&rest
之后定义的只用一个变量来使用,在传入的时候可以传入任意个参数。例如下面的例子
(defun foo (var1 var2 &optional op1 op2 &rest rest)
(list var1 var2 op1 op2 rest))
(foo 1 2) ;; ==> (1 2 nil nil nil)
(foo 1 2 3) ;; ==> (1 2 3 nil nil)
(foo 1 2 3 4) ;; ==> (1 2 3 4 nil)
(foo 1 2 3 4 5) ;; ==> (1 2 3 4 (5))
(foo 1 2 3 4 5 6) ;; ==> (1 2 3 4 (5 6))
从这个例子我们可以得出以下几个结论:
- 当可选参数没有提供时,在函数体里,对应的参数值都是 nil。我们可以通过判断是否为nil来判断用户是否传了参
&rest
要捕获后面所有传入的参数,所以它必须在参数列表的最后面,它的值是一个list- 当
&rest
与&optional
共存时,优先匹配&optional
参数,最后如果有剩余的参数则分配给&rest
教程原文中是有关于文档字符串的描述的。但是我想现在我作为一个菜鸟,将来要组织自己的配置也主要依靠拷贝粘贴别人现有的东西再组合,没有多少机会参与那种高大上的开源项目,自己将来弄的配置估计也没什么人用,而且我也会详细记录自己攒配置的过程,所以这里就不需要给函数写过于详细的文档说明。这里我就跳过这块了。如果有读者对这块感兴趣可以看原文。
函数调用
在编写程序的时候会有这种需求,一个框架负责处理大块的内容,比如数据解析、转发等等,它会预留一些接口来让用户在此基础之上处理自己的业务逻辑,比较典型的就是http server,或者gui程序框架。
在C/C++ 中一般会预留一些函数指针类型的参数进行回调或者提供接口供使用方重载实现自己的逻辑。在elisp中也有这样的操作,但是它就没有虚函数、虚基类或者函数指针的概念。
在elisp中通过符号调用一个函数使用的方法是 funcall
和 apply
。它们都是通过符号来调用函数的,唯一的区别在于如何处理传入参数。我们通过一个例子来看看它们有什么不同。我们还是用上面定义的foo函数来测试
(funcall 'foo 1 2 3 4 5 6) ;; ==> (1 2 3 4 (5 6))
(apply 'foo 1 2 3 4 5 6) ;; ==> error
(apply 'foo 1 2 3 4 '(5 6)) ;; ==> (1 2 3 4 (5 6))
(apply 'foo 1 2) ;; ==> error
(apply 'foo '(1 2)) ;; ==> (1 2 nil nil nil)
(apply 'foo 1 2 3 4) ;; ==> error
(apply 'foo 1 2 '(3 4)) ;; ==> (1 2 3 4 nil)
(apply 'foo 1 2 3 4 5) ;; ==> error
(apply 'foo 1 2 3 4 '(5)) ;; ==> (1 2 3 4 (5))
从上面的结果可以看出,funcall 直接按照对应函数定义的参数列表进行传参即可。而apply在传参的时候最后一个参数必须是list,并且在嗲用时会自动将list参数给展开并传入各个参数。
从上面的区别可以看出,如果在调用函数的时候,参数已经通过list进行了组织的话,那么使用apply更为合适,否则使用funcall。
宏
宏是lisp家族中一个非常重要,也非常灵活的内容,可以说宏是lisp的灵魂。之前在看到一些lisp相关的教程时都说,宏实现了利用代码生成代码,并且因为宏的存在导致lisp中扩展出了大量的方言。可以说没有宏,lisp就不是lisp了,或者说lisp就没这么灵活了。
但是C/C++中也有宏的概念,C/C++中的宏是在预处理阶段进行简单的文本替换,然后编译替换之后的结果。虽然利用宏,C/C++中可以实现很多非常复杂的功能,但是它远远没有lisp的宏灵活。
要详细了解宏的相关内容,我们先回忆一下之前介绍的elisp的知识。
首先elisp或者lisp的代码本身就是一颗语法树。它被写作一个list。也就是说list既可以作为代码执行,也可以作为数据,例如 (setq x 1)
它是一段代码。而 '(setq x 1)
它是一个列表,列表中有3个元素,分别是 setq
、x
、1
这么三个符号和数字。
再者elisp特有的符号系统,例如 x
表示一个变量,可以对它进行求值,'x
代表一个符号,根据前面所学的,我们可以通过符号找到符号中记录的值、函数、属性等等。
基于这两个内容,我们可以通过操作list来实现生成一段代码。例如下列的例子
(defun my-inc (var)
(list 'setq var (list '1+ var)))
(setq x 0)
(eval (my-inc 'x)) ;; ==> 1
上面的其实就是返回了一个list, (setq var (1+ var))
。后面我们通过 eval 来执行这个返回的list。需要注意的时,函数调用时会首先将变量进行求值,然后将值作为参数传入,但是这里我们希望并不希望传入一个具体的值,而是希望他能操作我们传入的变量值,并改变它,要做到这点需要传入一个符号。这里有点像C++ 中的引用传递
定义宏其实跟定义函数非常相似。我们只需要将关键字由 defun
改为 defmacro
。
(defmacro my-inc(var)
(list 'setq var (list '1+ var)))
(setq x 0)
(my-inc x) ;; ==> 1
我们发现宏与函数的一个不同点,函数中代码在函数被调用时执行,并且参数是在调用时进行求值并传入。而宏调用时需要展开它返回的表达式(或者这里直接就是一个list)。然后将参数作为符号传入。
宏最后需要返回一段可执行的list数据,如果没有返回,会影响展开执行,最终可能会报错,例如下面的例子
(defmacro my-inc (var)
(setq var (1+ var)))
(setq x 0)
(my-inc x) ;; ==> error
这里的问题在于这个宏定义的代码是一个直接执行的代码,并不是一个list,所以在调用它的时候会直接执行,但是又需要将参数作为符号绑定,所以它在被调用的时候会执行(setq 'x (1+ 'x))
这段代码,而这里的x是一个符号,无法直接对符号进行赋值,所以它会报x的类型错误。
这里已经显示出了,elisp中的宏与C/C++中宏的不同。首先C/C++中的宏只是简单的字符串替换,可以将它理解为它生成了新的C/C++源码的代码,它在预处理阶段来执行代码的替换。而elisp中并没有简单的进行替换,根据之前介绍lisp表达式的解析,其实宏返回的是一颗抽象语法树。在扩展宏的时候不断的进行抽象语法树的修改和重建,最后在执行的时候将传入的参数作为符号放入到这颗树中的对应节点。
我们可以使用 macroexpand
来查看宏展开的样子。
(defmacro bad-inc (var)
(setq var (1+ var)))
(macroexpand '(bad-inc 0)) ;; ==> 1
我们发现之前错误的实现并没有生成可执行的代码,而是直接返回一个常数。因为宏中的代码首先在展开的时候就已经执行了。相当于返回了 setq var (1+ 0)
的值,也就是1。
(defmacro my-inc (var)
(list 'setq var (list '1+ var)))
(macroexpand '(my-inc x)) ;; ==> (setq x (1+ x))
使用 macroexpand
可以使宏的编写变得容易一些。但是如果不能进行 debug 是很不方便的。在宏定义里可以引入 declare
表达式,它可以增加一些信息。目前只支持两类声明:debug
和 indent
。debug
可选择的类型很多,具体参考 info elisp - Edebug
一章,一般情况下用 t
就足够了。indent
的类型比较简单,它可以使用这样几种类型:
- nil 也就是一般的方式缩进
- defun 类似 def 的结构,把第二行作为主体,对主体里的表达式使用同样的缩进
- 整数 表示从第 n 个表达式后作为主体。比如 if 设置为 2,而 when 设置为 1
- 符号 这个是最坏情况,你要写一个函数自己处理缩进。
从前面的例子就可以看到,如果在定义宏的时候使用list
cons
等来构建list是非常麻烦的,一旦要构造非常复杂的程序,可能直接就歇菜了。为了方便,elisp中提供了一些符号来简化操作。
`
读作backquote,表示被它包裹的表达式都是quote,可以理解为它里面的直接构建了一个list- 如果希望它里面的某个位置不作为quote的一部分,而是直接作为列表的元素,可以使用
,
, 也就是它会对后面的内容进行求值 - 如果要让一个列表作为整个列表的一部分(slice),可以用 “,@”,它会将后面的内容作为列表参数依次添加到当前列表中。
我想起来了之前接触过的quote,也就是 '
。它表示后面的内容不进行求值,作为符号,虽然它也可以构造一个list,但是二者还是有些不同,例如
'(list x (+ 1 2)) ;; ==> (list x (+ 1 2))
`(list x (+ 1 2)) ;; ==> (list x (+ 1 2))
'(list x ,(+ 1 2)) ;; ==> (list x (\, (+ 1 2)))
`(list x ,(+ 1 2)) ;; ==> (list x 3)
(setq var '(2 3))
'(list x ,@var) ;; ==> (list x (\,@ var))
`(list x ,@var) ;; ==> (list x 2 3)
我们使用上面的方法稍微弄一个复杂一点的宏
(defmacro max(a b)
`(if (> ,a ,b)
,a
,b))
(max 4 5)
(max (1+ 2) (+ 3 6))
'(macroexpand '(max (1+ 2) (+ 3 6))) ;; ==> (if (> (1+ 2) (+ 3 6)) (1+ 2) (+ 3 6))
这里是一个经典的C/C++ 中的max宏。虽然实现不严谨,有一些副作用,但是可以从上面看到一些用法。
首先使用 `
表示返回一个列表,以供调用的时候进行展开。再者对于传入的a和b需要使用,
来表示需要求解它们的值,实现参数的绑定,否则将会得到一个错误,例如
(defmacro max(a b)
`(if (> a b)
a
b))
(macroexpand '(max 4 5))) ;; ==> (if (> a b) a + b)
如果将上述的 ,
全部替换成 ,@
就不太合适了,因为 ,@ 是将列表中的值取出来组成新的列表,并不会想 ,
那样进行求值。例如
(defmacro max(a b)
`(if (> ,@a ,@b)
,@a
,@b))
(macroexpand '(max (1+ 2) (+ 3 6))) ;; ==> (if (> 1+ 2 + 3 6) 1+ 2 + 3 6)
命令
emacs 运行时就是处于一个命令循环中,不断从用户那得到按键序列,然后调用对应命令来执行。emacs 中的命令可以说就是一个函数,它是一个特殊的函数,是里面包含了 interactive
表达式的函数。
这个表达式指明了这个命令的参数。比如下面这个命令
(defun say-hello (name)
(interactive "swhat's your name:")
(message "hello, %s" name))
当解释器加载了该函数之后就可以使用 M-x
来调用这个函数。我们根据提示输入一个名字,emacs会在minibuffer中输出一段话。
我们发现,在interactive 表达式后面跟的字符串前面多了一个 s
字符。我们可以通过这个多加的字符来控制命令参数的类型和行为,例如使用 s
表示字符串参数,n
表示数字参数,f 代表文件,r 代表区域。
interactive 的字符十分复杂,而且繁多。用的时候看 interactive 函数的文档还是很有必要的。但是不是所有时候都参数类型都能使用代码字符,而且一个好的命令,应该尽可能的让提供默认参数以让用户少花时间在输入参数上,这时,就有可能要自己定制参数。
首先学习和代码字符等价的几个函数。s 对应的函数是 read-string
,n
代表的是 read-file
,f代表的是 read-file-name
。其实大部分代码字符都是有这样对应的函数或替换的方法。我们可以使用这些方法来替代前面的代码字符,假如传入的是一个表达式,那么对表达式进行计算之后返回的列表元素就是命令的参数,例如我们用 read-string
来代替之前例子中的s
。
(defun say-hello (name)
(interactive (list (read-string "what's your name: ")))
(message "hello, %s" name))
教程 中还列举了一些常见的字符代表的函数,这里我就不列出来了。各位读者有兴趣的话也可以去看看。