按语:这是我在黑客帝国里为「不懂编程的人」写的系列文章的第六篇,整理于此。它的前一篇是《周而复始》,讲述了递归函数的周而复始可以作为程序的动力源。

Murphis 拿出两颗药丸,一颗红色,一颗蓝色,让 Neo 选。吃了红药丸,可以觉醒,看到真相;吃了蓝药丸,则可以继续浑噩地生活下去。

这样的程序,用 Emacs Lisp 该怎么写?

(defun murphis (答案)
  (cond
   ((string= 答案 "红药丸") '红药丸)
   ((string= 答案 "蓝药丸") '蓝药丸)
   (t (message "没有第三种选择!"))))

(murphis "红药丸")

用 Emacs 打开 init.el 文件,将上面的代码复制到 init.el 文件的尾部,然后将光标移动到 murphis 这个函数定义的末尾,执行 C-x C-e。这样 Emacs Lisp 解释器就知道了 murphis 这个函数的定义了。

接下来,(murphis "红药丸") 是对 murphis 函数进行求值。在《黑客帝国》里,等同于 Neo 对 Murphis 说,我要红药丸!倘若将光标移到这个表达式的末尾,然后执行 C-x C-e,结果就会在微缓冲区里显现 红药丸

在 Emacs Lisp 里,上述的 cond 表达式,就是所谓的条件表达式。

cond 表达式的形式如下:

(cond
 (<谓词 1> <表达式 1>)
 (<谓词 2> <表达式 2>)
 ... ... ...
 (<谓词 n> <表达式 n>))

什么是谓词?上文代码中的 (string= 答案 "红药丸") 就是谓词(Predicate)。

简单地说,谓词就是一个求值结果为真(t)或假(nil)的表达式。像 (string= 答案 "红药丸") 这样的表达式,string= 是一个函数,它会判断自己所接受的两个字符串原子是否相同,倘若相同,则结果为真,否则为假。

tnil 本身也能构成谓词。因为在 Emacs Lisp 里,表达式不一定必须是以 ( 开头,以 ) 结尾的语句。Emacs Lisp 的原子本身就可以构成表达式。

在上述代码中,'红药丸'蓝药丸 以及 "没有第三种选择!",它们都是原子,但也都是表达式。只要是表达式,Emacs Lisp 解释器就可以对它们进行求值,求值结果就是原子本身。

这里需要解释一下,'红药丸'蓝药丸 都是符号原子。之所以要以单引号 ' 作为前缀,是为了避免 Emacs Lisp 将它们视为函数或参数(变量)。不妨将符号原子视为标签,把它贴到函数上,它就是代表函数。把它贴到函数的参数(变量)上,它就代表参数。当这个标签的前面放上 ' 时,它就是一张未贴任何事物上的标签。用同样的办法,可以避免 Emacs Lisp 将列表视为函数求值表达式。例如,'(list 1 2 3),在 Emacs Lisp 解释器看来,它是由 list 这个符号原子与三个数字原子构成的列表,而不是 (list 1 2 3) 的求值结果 (1 2 3)

为什么要将 (string= 答案 "红药丸") 这样的表达式称为谓词呢?倘若将 string= 理解为 是(is),那么这个表达式的含义就变成了 is 答案 "红药丸",在这句话的后面再加上个问号的话,就变成了 is 答案 "红药丸"?。基于初中英语知识,可以发现这是个英文与汉语夹杂的一般疑问句。所以……差不多就这意思,略微了解一下即可。

在上述的 cond 表达式中,对谓词的求值顺序是从上而下,直到某个谓词的求值结果为真(t),便对该谓词所关联的表达式进行求值,所得结果即为 cond 表达式的求值结果。

倘若一个 cond 表达式中所有谓词的求值结果皆为假(nil),那么这个 cond 表达式的结果就是未定义。不过 Emacs Lisp 在遇到 cond 表达式的求值结果为未定义的时候,会以 nil 作为求值结果。

例如,

(defun murphis (答案)
  (cond
   ((string= 答案 "红药丸") '红药丸)
   ((string= 答案 "蓝药丸") '蓝药丸)))

(murphis "黑药丸")

(murphis "黑药丸") 的求值结果就是 nil

有的时候,只存在两种选择,非此即彼。像这样的选择,用 cond 表达式来模拟,有点繁琐,这时可以考虑用 如果...就...否则... 这样的表达式。例如:

(defun 齿轮 (m n)
  (if (< m n)
      (progn
        (insert (format "%d-" m))
        (齿轮 (+ m 1) n))
    (insert (format "%d\n" m))))

在推敲这段代码之前,先看一下 if 表达式的结构:

(if <谓词> <表达式 1> <表达式 2>)

这个结构可以这样解读,如果 <谓词> 为真,那么就对 <表达式 1> 求值,否则就对 <表达式 2> 求值。

将这个语法结构套到 齿轮 函数里的 if 表达式,结果是,如果 (< m n) 的求值结果为真,那么就对表达式 (progn (insert (format "%d-" m)) (齿轮 (+ m 1) n)) 进行求值,否则就对 (insert (format "%d\n" m)) 进行求值。这几个较长的表达式看上去有点复杂,需要有点耐心,逐层进行拆解。

首先来看 progn 表达式,它的作用是让 Emacs Lisp 解释器依序对一组并列的表达式进行求值。它的结构如下:

(progn
  <表达式 1>
  <表达式 2>
  ... ... ...
  < 表达式 n>)

由于 progn 本身也是一个表达式。按照 Emacs Lisp 解释器的法律,每个表达式必须有一个值。因此,progn 表达式会将它所囊括的这组表达式中的最后一个表达式的求值结果作为自身的结果。

实际上,list 表达式也能产生类似 progn 的效用。例如:

(list
  <表达式 1>
  <表达式 2>
  ... ... ...
  < 表达式 n>)

Emacs Lisp 解释器会对 list 囊括的这组表达式依序求值。只不过 list 返回的是这组表达式的全部的求值结果,亦即列表。

insert 函数,我们已经见识过多次了。虽然一直没对它做太多介绍,但是通过实践,不难发现,它可以在缓冲区里光标所在的位置插入文本——字符串原子。

format 函数,它可以用于构造字符串原子,只不过是以格式化的方式。所谓格式化,就相当于……做语文填空题。例如,(format "%d-" m),就相当于在 "( )-" 这句话的括号内填写一个数字。倘若你填 1,那么这句话就变成了 "1-"。同理 (format "%d\n" m)),倘若填上 10,结果就是 "10\n"。在字符串原子中,\n 这个字符表示换行,相当于你在文本编辑器里输入一行文本之后,单击了一次回车键。

还有一个很简单的表达式,(+ m 1),它的意思是 m + 1+ 是数字原子的运算符,它也是一个函数。类似的还有 -*/<>=>=<=。虽然这些运算符的用法不太符合我们在小学数学里学到的运算方法,但有的时候它也很有用。例如,你去超市买了许多东西,回家后想核对一下钱数,可以这样算 (+ 2.5 8.75 9 10 20 35.5)

弄懂了这些琐碎的函数的含义,齿轮 函数的定义差不多可以看懂了吧?这个函数最难看懂的部分其实是它在内部对自身进行求值——递归求值。

还记得那个 c-malloc 函数吗?

(defun c-malloc (name type n)
  (interactive
   (list (read-string "变量名称?")
     (read-string "类型?")
     (read-string "数量?")))
  (insert (format "%s *%s = malloc(%s * sizeof(%s));" type name n type)))

当我依序回答这个函数问我的三个问题之后,它就会在缓冲区里显现:

double *foo = malloc(n * sizeof(double));

这个函数有个问题,当 n 为 1 时,它会给出:

double *foo = malloc(1 * sizeof(double));

在 C 语言中,1 * sizeof(double) 表示拿 1 去乘一个数。小学生都知道,拿 1 去乘一个数就等于那个数本身。因此 c-malloc 若想表现自己已经小学毕业了,那么对于这样的情况,它应该给出:

double *foo = malloc(sizeof(double));

为此,需要对 c-malloc 略作改进:

(defun c-malloc (name type n)
  (interactive
   (list (read-string "变量名称?")
     (read-string "类型?")
     (read-string "数量?")))
  (if (string= n "1")
      (insert (format "%s *%s = malloc(sizeof(%s));" type name type))
    (insert (format "%s *%s = malloc(%s * sizeof(%s));" type name n type))))

以上,就是 Emacs Lisp 语言中常用的条件表达式的用法。在程序中,我们可以用它们来模拟「选择」。

一些热衷于从哲学层面探究人生意义的成功人士喜欢说这样的话,人生的意义,就在于你的选择!

他们若是略微懂一点编程,就会发现这样的话有些愚蠢。倘若人生的意义真的是在于选择,这就是变相地承认自己活在一个编制好的程序里。任何选择,在这个程序里,不过是一个谓词,而与这个谓词相关联的表达式早已在那里了。也就是说,无论你怎么选择,你的人生早已确定。因此,说人生的意义在于选择,无异于说自己相信天命所归,这就是所谓的宿命论。倘若一切都是程序安排好的结果,那么你选与不选,本质上并没有区别,你并没有权力不选择什么……

在我看来,人生的意义至少在于 debug。无论怎么看,去消除这个世界的 bug,总比被这个世界逼迫着整天作各种选择更有意义。

下一篇周游抑或毁灭世界


如果觉得我的文章对你有用,请随意赞赏
载入中...