Emacs折腾日记(三)——简单的elisp 入门
Emacs本身的使用并不复杂,利用帮助文档,差不多半小时左右就能把一些常见的操作方式和快捷键过一遍,剩下的就是慢慢使用并且熟悉了。
Emacs真正有价值的是它高度的客制化。任何人都可以利用elisp代码将Emacs改造成只属于自己的编辑器。会elisp 的不一定是高手,但是高手没有一个是不会elisp的。学习Emacs也绕不开elisp。下面我们就来简单的学点elisp
一个简单的 Hello Word
(message "hello world")
这是一个简单的elisp版本的hello world程序。麻雀虽小但是五脏俱全。从这个简单的程序来说我们可以看出lisp
的最大特点,就是以括号作为作为一个完整的表达式。曾今有一个段子说苏联的特工冒死偷到了美国阿波罗计划代码的最后一页,结果回来一看全是括号。这也鲜明的表达了一个lisp的特点,那就是大量的括号
我们简单的解析一下上面的代码,上面的代码调用了一个函数,函数的参数就是一个字符串的 hello world
。
我们可以打开Emacs,进入 scratch buffer,输入上述代码。之后可以将光标移动到代码尾部,按下快捷键 C-x C-e
来执行代码,或者使用 M-x
输入命令 eval-buffer
来看效果。我们可以看到,mini-buffer 位置出现了 "hello world" 的字符串
变量
我们可以使用 setq
来定义变量,它类似于C/C++ 中的=
,用来给变量赋值或者定义并初始化一个变量。
例如我们可以改一下上述的代码
(setq name "Emacs")
(message "hello, %s" name)
将上述代码输入到scratch buffer之后就需要依次在每行的最后执行 C-x C-e
或者直接执行eval-buffer
命令,这样就可以看到效果了
上面我们说,elisp的表达式是使用括号来表示的,但是如果在上面代码的基础上加上一句
(name)
此时就会报错,显示的错误为 void-function name
。我们的本意是想让解释器返回name的值,但是解释器将它作为了一个函数。通过这个错误我们能了解到,elisp基本的表达式中函数需要用括号括起来,但是变量自己本身被作为一个完整的表达式。
除了使用setq我们还可以使用 defvar
来定义变量,defvar
的使用如下
(defvar variable-name value
"variable document")
例如
(defvar name "Emacs"
"a defvar demo name")
name ;; ==> "Emacs"
我们将光标放到name上,按下 C-h v
可以看到关于name
的说明文档。
需要注意的是,defvar与setq 除了defvar可以指定变量的说明文档外,还有一个区别就是defvar在定义变量前,这个变量已经有值的话,defvar不会改变变量的值
例如下面的例子
(setq foo "foo")
(defvar foo "this is foo"
"document for variable foo")
(defvar bar "this is bar"
"document for variable bar")
foo ;; =>"foo"
bar ;; =>"this is bar"
C-x C-e(eval-last-sexp) treatsdefvarexpressions specially. Normally, evaluating adefvarexpression does nothing if the variable it defines already has a value. But this command unconditionally resets the variable to the initial value specified by thedefvar; this is convenient for debugging Emacs Lisp programs.defcustomanddeffaceexpressions are treated similarly. Note the other commands documented in this section, excepteval-defun, do not have this special feature.
上述英文翻译过来就是 eval-last-sexp 对 defvar 做了特殊处理,默认情况下 defvar 在变量有值的情况下不做任何操作,但是在这个命令中,defvar 会无条件的将变量值重置为它指定的值。主要是为了方便调试代码。同时 defcustom 和 deface 做了同样的操作。请注意在本节中记录的其他命令(eval-defun 除外)没有做这样的处理
这就解释了为什么我们使用 C-x C-e
执行的时候 foo
的值发生了改变
函数
函数的定义与使用
作为一门函数式编程语言,函数是elisp的一等公民。如何使用一个函数我们已经在前面的hello world程序中见识过了,那么如何定义一个函数呢?
定义一个函数使用 defun
关键字。它的语法如下
(defun func-name(args)
"document string"
body)
第一行代表一个函数名和函数的参数列表,第二行表示可以使用一个双引号包含函数的说明文档,Emacs是一个自文档的系统,后续我们可以查看这里写的文档。最后一行是函数的主体内容,例如下面的例子
(defun say-hello(name)
"say hello to define user"
(message "hello, %s" name))
(say-hello "emacs") ;; => "hello, emacs"
执行将会输出对应的信息。我们将光标移动到say-hello这个函数上,执行 C-h f
默认回车将会得到我们针对函数写的文档
lambda 表达式
其实像C++、Java、Python 之类的语言也有lambda表达式,它就是一个匿名函数。一般我们使用函数都是先定义同时给函数取一个名字,但是有时候我们仅仅需要一个临时函数作为参数或者仅仅只会在某些地方调用一次,这个时候就可以使用匿名函数。
lambda表达式的形式与defun
类似,它的使用规则如下
(lambda (args)
"document string"
body)
除了关键字变了,就是不用写函数名称了。
我们使用 funcall
来调用一个lambda表达式
(funcall (lambda (name)
(message "hello, %s" name)) "emacs")
我们执行它将会在mini-buffer
中看到显示的字符串信息
另外我们也可以将一个lambda
表达式赋值给一个变量,最后通过funcall 来调用
(setq say-hello (lambda (name)
(message "hello, %s" name)))
(funcall say-hello "emacs")
(say-hello "emacs") ;; error, void-function say-hello
知乎的大牛指出,这里原本有一个错误,在这里更正:
defun 会把函数值绑定到符号的 function-cell 上,setq 会绑定到符号的 value-cell 上
上述的说法,我查过 funcall
、value-cell
以及 function-cell
相关的文档,里面涉及到的一些知识点比较复杂,目前我还没有完全搞明白,而且把它贴出来作为入门来讲有点过于复杂了。
变量作用域
elisp默认使用 setq
定义的变量不管是在函数内还是函数外全都是全局变量,它们的作用域是全局作用域,例如
(defun say-hello ()
(setq name "Emacs")
(message "hello, %s" name))
(say-hello) ;;需要执行一下函数解释器才能执行到定义`name`变量的位置
(message name)
一般来说代码如果都是全局变量的话,会给代码的编写和维护带来很大的不便。elisp中同样支持定义局部变量,我们可以使用 let
和 let*
它们的用法类似
(let (bindings)
body)
其中的 bindings
可以是单个值,也可以是括号包裹的键值对。如果是单个值,则默认赋值nil,也就是空。如果是键值对,则将值赋值给对应的键。
let定义的变量只能作用在let语句块内,例如下面一个计算圆面积的函数,这里知乎的大牛告诉我,pi
是emacs中的内置变量,我采用PI来定义圆周率
(defun circle-area (radix)
(let ((PI 3.1415926)
area)
(setq area (* PI radix radix))
(message "半径为 %.2f 的圆的面积是 %.2f" radix area)))
(circle-area 3)
(message "%f" area) ;; error void-variable area
其中我们在let语句块中定义了两个变量,pi初始化为3.1415926,area 初始化为 nil
同样的,可以使用 let*
改写上面的程序
(defun circle-area (radix)
(let* ((PI 3.1415926)
area)
(setq area (* PI radix radix))
(message "半径为 %.2f 的圆的面积是 %.2f" radix area)))
(circle-area 3)
let*
与 let
的区别在于,let*
可以在binding时候使用前面已经定义过的变量。例如上面的代码可以改写成
(defun circle-area (radix)
(let* ((PI 3.1415926)
(area (* PI radix radix)))
(message "半径为 %.2f 的圆的面积是 %.2f" radix area)))
(circle-area 3)
我们使用 let
来改写一下这个程序,发现它会报错
(defun circle-area (radix)
(let ((PI 3.1415926)
(area (* PI radix radix)))
(message "半径为 %.2f 的圆的面积是 %.2f" radix area)))
(circle-area 3) ;; error void-variable PI
好了本篇也该结束了,本篇主要了解了基本的elisp语法,下面进行一下总结:
- 使用 defvar 和 setq 来定义全局变量,其中defvar可以给变量设置一个说明文档,我们使用
C-h v
来查看这个文档 - 使用 let 和 let* 来定义变量,其中 let* 可以在变量定义的时候使用前面定义过的变量
- 使用 defun 来定义函数
- 使用lambda 来定义一个lambda表达式,使用funcall 来调用lambda,lambda可以赋值给变量,后续使用funcall来调用