15分钟学会emacslisp

Table of Contents

背景

Lisp语言诞生的时候,就包含了9种新思想。其中一些我们今天已经习以为常,另一些则刚刚在其他高级语言中出现,至今还有2种是Lisp独有的。按照被大众接受的程度,这9种思想依次是:

  • 1. 条件结构(即"if-then-else"结构)。现在大家都觉得这是理所当然的,但是Fortran I就没有这个结构,它只有基于底层机器指令的goto结构。
  • 2. 函数也是一种数据类型。在Lisp语言中,函数与整数或字符串一样,也属于数据类型的一种。它有自己的字面表示形式(literal representation),能够储存在变量中,也能当作参数传递。一种数据类型应该有的功能,它都有。
  • 3. 递归。Lisp是第一种支持递归函数的高级语言。
  • 4. 变量的动态类型。在Lisp语言中,所有变量实际上都是指针,所指向的值有类型之分,而变量本身没有。复制变量就相当于复制指针,而不是复制它们指向的数据。
  • 5. 垃圾回收机制。
  • 6. 程序由表达式(expression)组成。Lisp程序是一些表达式区块的集合,每个表达式都返回一个值。这与Fortran和大多数后来的语言都截然不同,它们的程序由表达式和语句(statement)组成。区分表达式和语句,在Fortran I中是很自然的,因为它不支持语句嵌套。所以,如果你需要用数学式子计算一个值,那就只有用表达式返回这个值,没有其他语法结构可用,因为否则就无法处理这个值。后来,新的编程语言支持区块结构(block),这种限制当然也就不存在了。但是为时已晚,表达式和语句的区分已经根深蒂固。它从Fortran扩散到Algol语言,接着又扩散到它们两者的后继语言。
  • 7. 符号(symbol)类型。符号实际上是一种指针,指向储存在哈希表中的字符串。所以,比较两个符号是否相等,只要看它们的指针是否一样就行了,不用逐个字符地比较。
  • 8. 代码使用符号和常量组成的树形表示法(notation)。
  • 9. 无论什么时候,整个语言都是可用的。Lisp并不真正区分读取期、编译期和运行期。你可以在读取期编译或运行代码;也可以在编译期读取或运行代码;还可以在运行期读取或者编译代码。

在读取期运行代码,使得用户可以重新调整(reprogram)Lisp的语法;在编译期运行代码,则是Lisp宏的工作基础;在运行期编译代码,使得Lisp可以在Emacs这样的程序中,充当扩展语言(extension language);在运行期读取代码,使得程序之间可以用S-表达式(S-expression)通信,近来XML格式的出现使得这个概念被重新"发明"出来了。

REPL

> 度-求值-打印-循环:read-eval-print-loop

语法

  1: ;; 15分钟学会Emacs Lisp (v0.2a)
  2: ;;(作者:bzg,https://github.com/bzg
  3: ;;  译者:lichenbo,http://douban.com/people/lichenbo)
  4: ;;
  5: ;; 请先阅读Peter Norvig的一篇好文:
  6: ;; http://norvig.com/21-days.html
  7: ;; (译者注:中文版请见http://blog.youxu.info/21-days/)
  8: ;;
  9: ;; 之后安装GNU Emacs 24.3:
 10: ;;
 11: ;; Debian: apt-get install emacs (视具体发行版而定)
 12: ;; MacOSX: http://emacsformacosx.com/emacs-builds/Emacs-24.3-universal-10.6.8.dmg
 13: ;; Windows: http://ftp.gnu.org/gnu/windows/emacs/emacs-24.3-bin-i386.zip
 14: ;;
 15: ;; 更多信息可以在这里找到:
 16: ;; http://www.gnu.org/software/emacs/#Obtaining
 17: 
 18: ;; 很重要的警告:
 19: ;;
 20: ;; 按照这个教程来学习并不会对你的电脑有任何损坏
 21: ;; 除非你自己在学习的过程中愤怒地把它砸了
 22: ;; 如果出现了这种情况,我不会承担任何责任
 23: ;;
 24: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 25: ;;
 26: ;; 打开emacs
 27: ;;
 28: ;; 按~q~消除欢迎界面
 29: ;;
 30: ;; 现在请注意窗口底部的那一个灰色长条
 31: ;;
 32: ;; "*scratch*" 是你现在编辑界面的名字。
 33: ;; 这个编辑界面叫做一个"buffer"。
 34: ;;
 35: ;; 每当你打开Emacs时,都会默认打开这个scratch buffer
 36: ;; 此时你并没有在编辑任何文件,而是在编辑一个buffer
 37: ;; 之后你可以将这个buffer保存到一个文件中。
 38: ;;
 39: ;; 之后的"Lisp interaction" 则是表明我们可以用的某组命令
 40: ;;
 41: ;; Emacs在每个buffer中都有一组内置的命令
 42: ;; 而当你激活某种特定的模式时,就可以使用相应的命令
 43: ;; 这里我们使用 ~lisp-interaction-mode~ ,
 44: ;; 这样我们就可以使用内置的Emacs Lisp(以下简称Elisp)命令了。
 45: 
 46: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 47: ;;
 48: ;; 分号是注释开始的标志
 49: ;;
 50: ;; Elisp 是由符号表达式构成的 (即"s-表达式"或"s式"):
 51: (+ 2 2)
 52: 
 53: ;; 这个s式的意思是 "对2进行加2操作".
 54: 
 55: ;; s式周围有括号,而且也可以嵌套:
 56: (+ 2 (+ 1 1))
 57: 
 58: ;; 一个s式可以包含原子符号或者其他s式
 59: ;; 在上面的例子中,1和2是原子符号
 60: ;; (+ 2 (+ 1 1)) 和 (+ 1 1) 是s式.
 61: 
 62: ;; 在  ~lisp-interaction-mode~ 中你可以计算s式.
 63: ;; 把光标移到闭括号后,之后按下ctrl+j(以后简写为 ~C-j~ )
 64: 
 65: (+ 3 (+ 1 2))
 66: ;;           ^ 光标放到这里
 67: ;; 按下 ~C-j~ 就会输出 6
 68: 
 69: ;;  ~C-j~ 会在buffer中插入当前运算的结果
 70: 
 71: ;; 而 ~C-xC-e~ 则会在emacs最底部显示结果,也就是被称作"minibuffer"的区域
 72: ;; 为了避免把我们的buffer填满无用的结果,我们以后会一直用 ~C-xC-e~
 73: 
 74: ;;  ~setq~ 可以将一个值赋给一个变量
 75: (setq my-name "Bastien")
 76: ;;  ~C-xC-e~ 输出 "Bastien" (在 mini-buffer 中显示)
 77: 
 78: ;;  ~insert~ 会在光标处插入字符串:
 79: (insert "Hello!")
 80: ;;  ~C-xC-e~ 输出 "Hello!"
 81: 
 82: ;; 在这里我们只传给了insert一个参数"Hello!", 但是
 83: ;; 我们也可以传给它更多的参数,比如2个:
 84: 
 85: (insert "Hello" " world!")
 86: ;;  ~C-xC-e~ 输出 "Hello world!"
 87: 
 88: ;; 你也可以用变量名来代替字符串
 89: (insert "Hello, I am " my-name)
 90: ;;  ~C-xC-e~ 输出 "Hello, I am Bastien"
 91: 
 92: ;; 你可以把s式嵌入函数中
 93: (defun hello () (insert "Hello, I am " my-name))
 94: ;;  ~C-xC-e~ 输出 hello
 95: 
 96: ;; 现在执行这个函数
 97: (hello)
 98: ;;  ~C-xC-e~ 输出 Hello, I am Bastien
 99: 
100: ;; 函数中空括号的意思是我们不需要接受任何参数
101: ;; 但是我们不能一直总是用my-name这个变量
102: ;; 所以我们现在使我们的函数接受一个叫做"name"的参数
103: 
104: (defun hello (name) (insert "Hello " name))
105: ;;  ~C-xC-e~ 输出 hello
106: 
107: ;; 现在我们调用这个函数,并且将"you"作为参数传递
108: 
109: (hello "you")
110: ;;  ~C-xC-e~ 输出 "Hello you"
111: 
112: ;; 成功!
113: 
114: ;; 现在我们可以休息一下
115: 
116: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
117: ;;
118: ;; 下面我们在新的窗口中新建一个名为 "*test*" 的buffer:
119: 
120: (switch-to-buffer-other-window "*test*")
121: ;;  ~C-xC-e~ 这时屏幕上会显示两个窗口,而光标此时位于*test* buffer内
122: 
123: ;; 用鼠标单击上面的buffer就会使光标移回。
124: ;; 或者你可以使用  ~C-xo~ 使得光标跳到另一个窗口中
125: 
126: ;; 你可以用  ~progn~命令将s式结合起来:
127: (progn
128:   (switch-to-buffer-other-window "*test*")
129:   (hello "you"))
130: ;;  ~C-xC-e~ 此时屏幕分为两个窗口,并且在*test* buffer中显示"Hello you"
131: 
132: ;; 现在为了简洁,我们需要在每个s式后面都使用 ~C-xC-e~来执行,后面就不再说明了
133: 
134: ;; 记得可以用过鼠标或者 ~C-xo~ 回到*scratch*这个buffer。
135: 
136: ;; 清除当前buffer也是常用操作之一:
137: (progn
138:   (switch-to-buffer-other-window "*test*")
139:   (erase-buffer)
140:   (hello "there"))
141: 
142: ;; 也可以回到其他的窗口中
143: (progn
144:   (switch-to-buffer-other-window "*test*")
145:   (erase-buffer)
146:   (hello "you")
147:   (other-window 1))
148: 
149: ;; 你可以用  ~let~ 将一个值和一个局部变量绑定:
150: (let ((local-name "you"))
151:   (switch-to-buffer-other-window "*test*")
152:   (erase-buffer)
153:   (hello local-name)
154:   (other-window 1))
155: 
156: ;; 这里我们就不需要使用  ~progn~ 了, 因为  ~let~ 也可以将很多s式组合起来。
157: 
158: ;; 格式化字符串的方法:
159: (format "Hello %s!\n" "visitor")
160: 
161: ;; %s 是字符串占位符,这里被"visitor"替代.
162: ;; \n 是换行符。
163: 
164: ;; 现在我们用格式化的方法再重写一下我们的函数:
165: (defun hello (name)
166:   (insert (format "Hello %s!\n" name)))
167: 
168: (hello "you")
169: 
170: ;; 我们再用 ~let~ 新建另一个函数:
171: (defun greeting (name)
172:   (let ((your-name "Bastien"))
173:     (insert (format "Hello %s!\n\nI am %s."
174: 		    name       ; the argument of the function
175: 		    your-name  ; the let-bound variable "Bastien"
176: 		    ))))
177: 
178: ;; 之后执行:
179: (greeting "you")
180: 
181: ;; 有些函数可以和用户交互:
182: (read-from-minibuffer "Enter your name: ")
183: 
184: ;; 这个函数会返回在执行时用户输入的信息
185: 
186: ;; 现在我们让 ~greeting~ 函数显示你的名字:
187: (defun greeting (from-name)
188:   (let ((your-name (read-from-minibuffer "Enter your name: ")))
189:     (insert (format "Hello!\n\nI am %s and you are %s."
190: 		    from-name ; the argument of the function
191: 		    your-name ; the let-bound var, entered at prompt
192: 		    ))))
193: 
194: (greeting "Bastien")
195: 
196: ;; 我们让结果在另一个窗口中显示:
197: (defun greeting (from-name)
198:   (let ((your-name (read-from-minibuffer "Enter your name: ")))
199:     (switch-to-buffer-other-window "*test*")
200:     (erase-buffer)
201:     (insert (format "Hello %s!\n\nI am %s." your-name from-name))
202:     (other-window 1)))
203: 
204: ;; 测试一下:
205: (greeting "Bastien")
206: 
207: ;; 第二节结束,休息一下吧。
208: 
209: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
210: ;;
211: ;; 我们将一些名字存到列表中:
212: (setq list-of-names '("Sarah" "Chloe" "Mathilde"))
213: 
214: ;; 用  ~car~ 来取得第一个名字:
215: (car list-of-names)
216: 
217: ;; 用  ~cdr~取得剩下的名字:
218: (cdr list-of-names)
219: 
220: ;; 用  ~push~把名字添加到列表的开头:
221: (push "Stephanie" list-of-names)
222: 
223: ;; 注意:  ~car~ 和  ~cdr~ 并不修改列表本身, 但是  ~push~ 却会对列表本身进行操作.
224: ;; 这个区别是很重要的: 有些函数没有任何副作用(比如 ~car~)
225: ;; 但还有一些却是有的 (比如  ~push~).
226: 
227: ;; 我们来对 ~list-of-names~列表中的每一个元素都使用hello函数:
228: (mapcar ~hello list-of-names)
229: 
230: ;; 将  ~greeting~ 改进,使的我们能够对 ~list-of-names~中的所有名字执行:
231: (defun greeting ()
232:     (switch-to-buffer-other-window "*test*")
233:     (erase-buffer)
234:     (mapcar ~hello list-of-names)
235:     (other-window 1))
236: 
237: (greeting)
238: 
239: ;; 记得我们之前定义的  ~hello~ 函数吗? 这个函数接受一个参数,名字。
240: ;;  ~mapcar~ 调用  ~hello~, 并将 ~list-of-names~作为参数先后传给 ~hello~
241: 
242: ;; 现在我们对显示的buffer中的内容进行一些更改:
243: 
244: (defun replace-hello-by-bonjour ()
245:     (switch-to-buffer-other-window "*test*")
246:     (goto-char (point-min))
247:     (while (search-forward "Hello")
248:       (replace-match "Bonjour"))
249:     (other-window 1))
250: 
251: ;; (goto-char (point-min)) 将光标移到buffer的开始
252: ;; (search-forward "Hello") 查找字符串"Hello"
253: ;; (while x y) 当x返回某个值时执行y这个s式
254: ;; 当x返回 ~nil~ (空), 退出循环
255: 
256: (replace-hello-by-bonjour)
257: 
258: ;; 你会看到所有在*test* buffer中出现的"Hello"字样都被换成了"Bonjour"
259: 
260: ;; 你也会得到以下错误提示: "Search failed: Hello".
261: ;;
262: ;; 如果要避免这个错误, 你需要告诉  ~search-forward~ 这个命令是否在
263: ;; buffer的某个地方停止查找, 并且在什么都没找到时是否应该不给出错误提示
264: 
265: ;; (search-forward "Hello" nil t) 可以达到这个要求:
266: 
267: ;;  ~nil~ 参数的意思是 : 查找并不限于某个范围内
268: ;;  ~t~ 参数的意思是: 当什么都没找到时,不给出错误提示
269: 
270: ;; 在下面的函数中,我们用到了s式,并且不给出任何错误提示:
271: 
272: (defun hello-to-bonjour ()
273:     (switch-to-buffer-other-window "*test*")
274:     (erase-buffer)
275:     ;; 为 ~list-of-names~中的每个名字调用hello
276:     (mapcar ~hello list-of-names)
277:     (goto-char (point-min))
278:     ;; 将"Hello" 替换为"Bonjour"
279:     (while (search-forward "Hello" nil t)
280:       (replace-match "Bonjour"))
281:     (other-window 1))
282: 
283: (hello-to-bonjour)
284: 
285: ;; 给这些名字加粗:
286: 
287: (defun boldify-names ()
288:     (switch-to-buffer-other-window "*test*")
289:     (goto-char (point-min))
290:     (while (re-search-forward "Bonjour \\(.+\\)!" nil t)
291:       (add-text-properties (match-beginning 1)
292: 			   (match-end 1)
293: 			   (list ~face ~bold)))
294:     (other-window 1))
295: 
296: ;; 这个函数使用了  ~re-search-forward~:
297: ;; 和查找一个字符串不同,你用这个命令可以查找一个模式,即正则表达式
298: 
299: ;; 正则表达式 "Bonjour \\(.+\\)!" 的意思是:
300: ;; 字符串 "Bonjour ", 之后跟着
301: ;; 一组           |  \\( ... \\) 结构
302: ;; 任意字符       |  . 的含义
303: ;; 有可能重复的   |  + 的含义
304: ;; 之后跟着 "!" 这个字符串
305: 
306: ;; 准备好了?试试看。
307: 
308: (boldify-names)
309: 
310: ;;  ~add-text-properties~ 可以添加文字属性, 比如文字样式
311: 
312: ;; 好的,我们成功了!
313: 
314: ;; 如果你想对一个变量或者函数有更多的了解:
315: ;;
316: ;; C-h v 变量 回车
317: ;; C-h f 函数 回车
318: ;;
319: ;; 阅读Emacs Lisp官方文档:
320: ;;
321: ;; C-h i m elisp 回车
322: ;;
323: ;; 在线阅读Emacs Lisp文档:
324: ;; https://www.gnu.org/software/emacs/manual/html_node/eintr/index.html
325: 
326: ;; 感谢以下同学的建议和反馈:
327: ;; - Wes Hardaker
328: ;; - notbob
329: ;; - Kevin Montuori
330: ;; - Arne Babenhauserheide
331: ;; - Alan Schmitt
332: ;; - spacegoing
333: 

Date: 2022-05-27 Fri 15:29