Elisp 12(兔子洞)

前言:不知多久能学会 Elisp
上一章:动态模块
从本章开始,进入这份 Elisp 教程的第三部分。这部分内容侧重于应用,在倘若不得不引入没学过的 Elisp 语法之时,则说明所侧重的应用必定是好的。
一个游戏 现在考虑来写一个很简单的小游戏:在一个一维的世界里寻找兔子洞。
该如何表示这个一维世界呢?用缓冲区便可表示。该如何在这个世界里行走呢?在缓冲区内移动插入点。
在这个世界里,如何表示兔子洞呢?这需要三思。
缓冲区变量 我想用两个既不是全局变量也不是局部变量的变量来表示兔子洞的入口和出口。在 Elisp 语言里,这样的变量可以是缓冲区变量。下面是这两个变量的定义:

(setq hole-entrance "@#") (setq hole-exit "#@")(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (make-local-variable 'hole-exit))

虽然 hole-entrancehole-exit 一开始是定义成了全局变量,但是在 Elisp 解释器执行了 find-file 之后,当前缓冲区便是存放 world.txt 文件内容的缓冲区,这两个变量在 make-local-variable 的作用下,就变成了当前缓冲区内的变量。
任何一个缓冲区都能直接使用同一个全局变量,也将它变成自己的变量,但是各个缓冲区都会觉得自己可以独占它,修改它的值,而且这种修改对其他缓冲区没有任何影响。就像是一个人同时出现在多个世界里,他不知道其他世界里的自己是怎样的境况。
简单的世界 现在,已经有了兔子洞的入口和出口的表示形式了,因此可以为上述的游戏随便构造一个世界:
********@#一个兔子洞#@********@#又 一个兔子洞#@********************** **** @# 这也是一个兔子洞 #@****************

这个世界里,有三个兔子洞。我将这个世界保存在文本文件 world.txt 里,待 Elisp 程序使用 (find-file) 将其载入缓冲区。接下来的任务便是如何在存放这个世界的缓冲区内找出这些兔子洞。
search-forward 清楚了要解决的问题是什么,还有清楚自己有哪些资源可以利用以及如何利用,此二者的充分结合,便诞生了算法。要找出兔子洞,可以利用 Elisp 函数 search-forward,同之前用过的 re-search-forward 相似,可在缓冲区内前向递进搜索与指定文本相匹配的文本,只是后者支持正则表达式匹配。在寻找兔子洞的过程中,不需要用正则表达式,因此用 search-forward 更为适合。
现在试试 search-forward
(setq hole-entrance "@#") (setq hole-exit "#@")(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (search-forward hole-entrance))

Elisp 解释器对上述程序的求值结果是 11,这正是第一个兔子洞入口 @# 之后的位置,亦即 search-forward 返回的结果是缓冲区内与 hole-entrance 的值匹配的文本的末尾位置。
如果 search-forward 在缓冲区内未能发现与指定文本相匹配的文本,默认情况下它会报错。我觉得动不动就报错,太小题大作了,找不到就找不到,让它返回 nil 就好了,这需要将 search-forward 的第三个参数设为 t,即
(search-forward hole-entrance nil t)

要设置第三个参数,不得不设置第二个参数。从现在开始要记住,Elisp 函数的参数允许可选,但是倘若想设置其中的某个参数,但是又不知它前面的可选参数如何设置的时候,就将它们设为 nil 即可,因为 nil 是所有变量的默认值,函数的参数是变量。一旦 search-forward 能够返回 nil,便可以通过条件表达式,去接管这种查找失败的情况。
由于 search-forward 不仅能返回缓冲区内匹配文本的末尾位置,同时也将插入点移动到这一位置,因此 search-forward 便继续搜索下一个兔子洞,亦即
(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (make-local-variable 'hole-exit) (if (search-forward hole-entrance nil t) (search-forward hole-entrance nil t)))

可以发现第二个兔子洞的入口。
同理,下面的代码能发现第三个兔子洞的入口:
(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (make-local-variable 'hole-exit) (if (search-forward hole-entrance nil t) (if (search-forward hole-entrance nil t) (search-forward hole-entrance))))

搜索全部的兔子洞 上一节反复运用 search-forward 搜索兔子洞入口的过程显然是一个典型的迭代过程,因此可以用 while 表达式简化,即
(load "newbie")(setq hole-entrance "@#") (setq hole-exit "#@")(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (make-local-variable 'hole-exit) (let (@-extrance) (while (setq @-entrance (search-forward hole-entrance nil t)) (princ\n (format "兔子洞入口:%d" @-entrance)))))

还记得 newbie.el 库和 princ\n 宏吗?忘记了,就再回顾一下前面第 10 章的内容吧!
执行这个程序,在我的机器上的输出为
Loading /home/garfileo/.my-scripts/elisp/newbie.el (source)... 兔子洞入口:11 兔子洞入口:28 兔子洞入口:67

注意,setq 可以用作条件表达式,因为它可返回变量绑定的值,要么是 nil,要么不是 nil,后者与 t 等价。
对上述程序略作修改,
(load "newbie")(setq hole-entrance "@#") (setq hole-exit "#@")(progn (find-file "world.txt") (make-local-variable 'hole-entrance) (make-local-variable 'hole-exit) (let (@-extrance @-exit) (while (progn (setq @-entrance (search-forward hole-entrance nil t)) (setq @-exit (search-forward hole-exit nil t))) (princ\n (format "兔子洞 (%d %d)" @-entrance @-exit)))))

便可找出所有兔子洞的入口和出口了,因为在我的机器上的输出结果是
Loading /home/garfileo/.my-scripts/elisp/newbie.el (source)... 兔子洞 (11 18) 兔子洞 (28 37) 兔子洞 (67 98)

结语 【Elisp 12(兔子洞)】游戏远未结束,兔子洞是个很复杂的所在。

    推荐阅读