elisp
现代编辑器一般都提供了一些脚本语言用于用户自己的定制,而Emacs毫无疑问是其中的佼佼者。elisp是Emacs的灵魂所在,不会写elisp也就无法体会到Emacs所提供的灵活自由。
lisp for "List Processor",elisp是lisp专用于emacs的方言。lisp的标志性语法结构是它的括号,这里不多吐槽。下面简单介绍一下配置所需要了解的语法知识。
在学习的时候,可以打开scratch buffer,在其中试一下。将光标移动到表达式的结尾按下C-x C-e可以对前面的表达式进行求值。
开始
lisp的语法与主流语言有两个标志性的区别:
前缀操作符
即操作符在表达式的最前面,好处首先就是没有了其他语言的“优先级”问题。
(+ 3 4)
- 括号
lisp一直为人诟病的语法结构。习惯了还好,无非就是作用域的结构。当然如果习惯了python这种从设计上就去括号化的语言,可能会对这不爽。
elisp作为lisp的方言,以我的观点来看不用太深入学习,因为只是作为配置的话并用不到多么高深的语言技巧与抽象层次,所以只要把基本的一些语法掌握了就可以了。
数据类型
基本数据类型
3 ;; int 3.5 ;; float "string" ;; string t ;; bool nil ;; 空
符号
如SICP中所述,编程最重要的两个概念就是抽象和分层。简单来说,抽象即命名;复杂点说,抽象即将事物的复杂性隐去,而以其名称对其操作。lisp中用符号(symbol)来完成这一操作。elisp中,数据与函数分别在不同的命名空间(scheme只有一个命名空间,详见:stackoverflow),即函数与变量可以具有相同的名字。
全局变量:
(setq a1 3) ;; -> 3 (defvar a1 3 "temp varlue") ;; -> 3
在行末按下C-x C-e,minibuffer会显示"3 (#o3, #x3, ?\C-c)"。此时,在整个emacs的全局都可以访问到一个名为a1的变量,其值为3。defvar跟setq功能相同,只是多了个可选的文档字符串。这在编写插件的时候有用,自己维护配置可以直接使用setq。
局部变量:
(let ((a2 3) a3) (setq a3 1) (message "%s %s" a2 a3)) ;; -> 3 1 (let* ((a2 3) (a3 a2)) (message "%s %s" a2 a3)) ;; -> 3 3
let结构有两种:let与let*。let的结构是这样的:
(let ((var1 val1) (var2 val2) ...) body)
第一个列表用于临时变量的声明赋值;后面的语句是body,它们能使用上面声明的变量。当超出let的括号时,这些临时变量就被释放了。
let中变量列表的赋值顺序是不定的,因此在第一个例子里a3无法使用a2的值;而let*中后面变量的声明可以使用前面变量的值。
复合数据结构
如果说抽象是纵向的复杂性的话,复合数据就是横向的扩展。《道德经》有云:“道生一,一生二,二生三,三生万物”。lisp复合数据结构的构建向我们展示了这一点。
空表:
'() == (quote ()) == nil
可以看到一个',这个符号被称为引用符(quote)。它的作用是表示后面的表不立即求值,而把其当作一个字面值处理。例如:
(+ 1 2) ;; -> 3 '(+ 1 2) ;; -> (+ 1 2) (setq testquote '(+ 1 2)) (apply testquote) ;; 3
两个基本类型的数据可以组合成一个称为序对(cons cell)的结构:
(setq two (cons 1 2)) ;; (1 . 2) (car two) ;; 1 (cdr two) ;; 2
cons用于构建序对,car用于取前面的元素,cdr用于取后面的元素(序对与list的区别就是list的最后一个元素为nil)。而按照这种形式可以构成任意长度的表:
(setq three (cons 1 (cons 2 (cons 3 nil)))) ;; 等价于 (setq three (list 1 2 3)) (car three) ;; 1 (cdr three) ;; (2 3)
因此可以把list看成由很多序对组成的链表,其中每个序对的cdr指向另一个序对,最后一个序对的cdr指向空表。list函数可以看作一个语法糖。
alist
那么字典怎么实现呢,即以key的形式直接访问元素?lisp中提供了一种称为association list(a-list)的结构来达到这种效果。它的结构是这样的:
(setq color-alist '((red . "ff0000") (green . "00ff00") (blue . "0000ff")))
而使用内置的assoc函数可以进行取操作:
(setq green-item (assoc 'green color-alist)) -> (green . "00ff00") ;; item (car green-item) -> green ;; key (cdr green-item) -> "00ff00" ;; value
alist中对于key-value的替换是这样做的:
(add-to-list 'color-alist '(green . "00ff01")) -> ((green . "00ff01") (red . "ff0000") (green . "00ff00") (blue . "0000ff")) (cdr (assoc 'green color-alist)) -> "00ff01"
即前面的同key的元素会替换后面的。其他操作见官方文档。
控制结构
共有三种,分别是顺序、条件和循环(scheme没有循环,而用递归代替了循环)。
顺序
(progn expr1 expr2 ...)
progn的作用就是将多条表达式合并成一条,返回值为最后执行的语句;在有的结构里必须这么写,例子见下面的if结构。还有个prog1,它的返回值是第一条表达式的值。
条件
if
(if test then else1 else2 ...)
第一个list是条件,第二个list是t的时候执行的表达式,后面跟着的其他语句是nil的时候执行的表达式。返回值为最后执行的语句。例如:
(let ((a2 3)) (if (> a2 1) (progn (setq a2 2) (message "Right %s" a2)) (message "Wrong %s" a2))) ;; -> Right 2
还有两个简化版本的if:
when
没有else分支的if:
(let ((a2 3)) (when (> a2 1) (message "hello")))
unless
if的else分支:
(let ((a2 3)) (unless (< a2 1) (message "hello")))
cond
多条件语句,返回值为最后执行的语句。
(cond ((test1 body11 body12 ...) (test2 body21 body22 ...) ... (t bodyn1 bodyn2 ...)))
例如:
(let ((a4 4)) (cond ((> a4 3) (message "1") (message ">")) ((= a4 3) (message "1") (message "=")) ((< a4 3) (message "1") (message "<")))) ;; -> ;; 1 ;; > ;; ">"
逻辑语句
逻辑语句是经常使用的一种控制语句,返回值为最后执行的语句。
and会顺序执行表达式,直到遇到nil。所以返回值为nil或者最后一个语句。通常,
(if expr1 (if expr2 .. (if exprn-1 exprn)))
简写作
(and expr1 exp2 ...)
or会顺序执行表达式,直到遇到一个非nil。所以返回值为第一个非nil的值。
(if a a b)
简写作
(or a b)
not为非:
(not expr)
循环
while
返回值为nil。
(while test body1 body2 ...)
例如:
(let ((count 3)) (while (> count 0) (message "%d" count) (setq count (1- count)))) ;; 3 ;; 2 ;; 1 ;; nil
dolist
elisp提供了更方便的对list的遍历方法,返回值为nil。
(let ((templist '(1 2 3))) (dolist (var templist) (message "%d" var))) ;; 3 ;; 2 ;; 1 ;; nil
函数
按照scheme的看法,函数与数据并没有什么区别,但elisp中还是有区别的。
(setq func1 (lambda () (message "hello"))) (funcall func1) ;; scheme可以直接调用 ;; (func1)
elisp提供了更显式的函数定义方式:
(defun func1 () (message "hello")) (func1) ;; -> "hello"
这两种方式在elisp中的符号分别储存在不同的命名空间。现在我们仔细看一下defun。
(defun name (arg1 arg2) "document string" (interactive) body)
一个常见的defun是这样的。第一个元素为函数名,后面的列表是参数列表,参数可以使用&rest和&optional修饰;下面有一个可选的文档字符串以及一个可选的声明列表,最常见的就是(interactive),表示这个函数是一个可交互的函数,即用户可以通过关联快捷键或者M-x的方式直接调用该函数。对于不需要名字的函数,可以使用lambda表达式:
(bind-key (kbd "C-c C-1") (lambda () (interactive) (message "hello")) global-map)
修饰
可以在不修改源代码的情况下对函数进行修饰,具体参照文档。在插件之间相互冲突,或者希望微调第三方插件时这个功能很好用。
修饰可以对在函数调用前对其参数进行修改;或者修改函数的返回值;我们经常用修饰对第三方插件的一些bug或者其他冲突做修正而不必改动插件内的代码。这里提供一个例子:
(defun example-func (p1 p2 p3) (message "%d %d %d" p1 p2 p3)) (defun example-advice (oldfunc &rest args) (setcar (nthcdr 1 args) 0) (apply oldfunc args)) (example-func 1 2 3) ;; -> "1 2 3" (advice-add 'example-func :around 'example-advice) (example-func 1 2 3) ;; -> "1 0 3"
宏
在配置中基本上用不到,我实际上也并不是很熟悉。从概念上来说就是用来生成代码的代码。据说是lisp真正强大的地方,有兴趣可以自己研究。
作用域
elisp的作用域默认为dynamic binding,具体参照wiki。