roots of lisp 阅读笔记

Table of Contents

  1. 七个原始操作符
  2. 函数的表示
  3. 一些函数
  4. 一个惊喜
    1. 情况1
    2. 情况2和 evcon. 函数
    3. 情况4与 evlis. 函数
    4. 情况3
  5. 后记
  6. 原文代码

最近读了The Roots of Lisp中文翻译版本,最后的 eval 函数读了挺久才读懂,因此写下这篇阅读笔记,就当记录下自己的思考过程。文中的引用部分均来自于翻译版本,在此向中文版本的翻译者表达感谢。

我是直接用 Emacs Lisp 来运行这些 Lisp 代码的,因为 Emacs Lisp 是一种 Lisp-2,所以 lambda 那个地方有些代码会不同。还有原始的 label 标签好像已经被弃用了,本来想着自己写个宏把 label 翻译成 defun 的形式,奈何水平太差,最后也就放弃了 label 的相关内容。不过这并不影响最后的 eval 函数的实现,而且这个 eval 是可以解析带 label 的语句的。

我的 Emacs版本: GNU Emacs 29.0.50 (build 1, x86_64-w64-mingw32)

七个原始操作符

首先是 Lisp 最基础的概念,表达式。

表达式或是一个原子(atom),它是一个字母序列(如 foo),或是一个由零个或多个表达式组成的表(list), 表达式之间用空格分开, 放入一对括号中.

之后是七个原始操作符,这一部分没什么好说的,理解定义就行。

  • quote
  • atom
  • eq
  • car
  • cdr
  • cons
  • cond

函数的表示

首先因为 Emacs lisp 是 Lisp-2,所以这一部分有一个表达式需要稍作修改才能在 Emacs 里运行。原文中给出调用 lambda 函数的表达式为:

    ((lambda (f) (f '(b c)))
     '(lambda (x) (cons 'a x)))

这个表达式直接运行会报错。修改之后的版本为:

    ((lambda (f) (funcall f '(b c)))
     '(lambda (x) (cons 'a x)))

其实就是加了一个 funcall1 。不涉及把函数作为参数传递的 lambda 是不受影响的,可以直接在 Emacs 里面运行 ((lambda (x) (cons x '(b))) 'a)

文章紧接着介绍了 subst 这个函数,不过这个 label 的版本实在不知道怎么在 Emacs 里实现了,就略过了。文章中给出的 defun 形式的定义是可以正常运行的。

一些函数

这个部分给出的函数都可以在 Emacs 里运行,不需要做额外的修改。

  • null
  • and
  • not
  • append
  • pair
  • assoc

这部分我做的笔记有两点。首先 pair 函数在两个列表长度不相等的时候会直接截断,在一个参数为原子的时候也会直接返回 nil 。可以试试下面这几个式子。

    (pair. '(x y z) '(a b c))
    (pair. '(x y z) '(a b c d))
    (pair. 'x '(a b c))

然后是 assoc 函数,原文中给出的定义如下:

    (defun assoc. (x y)
      (cond ((eq (caar y) x) (cadar y))
            ('t (assoc. x (cdr y)))))

这个定义中,如果 y 中不存在键 x ,这个函数会陷入死循环。我修改了一下,在开头加了一句对 y 是否为空的判断,这样在找不到键的时候函数会返回 nil ,让其表现符合 Common-Lisp。修改后的定义如下:

    (defun assoc. (x y)
      (cond ((null. y) nil)
            ((eq (caar y) x) (cadar y))
            ('t (assoc. x (cdr y)))))

另外无论是原文定义还是我修改后的定义,如果 xnil ,二者都会返回 nil ,不过逻辑不太相同。在这个情况下,经过递归调用,表达式最终会变为 (assoc. nil nil) 。我的定义中判断此时 ynil ,于是返回 nil ,原文的定义则是判断 (eq (caar y) x) 成立(此时 xy 都是 nil ),于是返回了 (cadar y) ,也就是 nil 。不过无论逻辑如何,最终表现出来的结果都是一样的。

下面有四个关于 assoc. 的测试表达式,前两个表达式没什么问题,第三个表达式原文定义会陷入死循环,我修改的版本会返回 nil 。最后一个表达式是关于 nil 的测试。另外原文定义是完全满足后续定义 eval 的需求的,因此抛开死循环这个问题,我仍然采用原文的定义进行后续实验。

    (assoc. 'x '((x a) (y b)))
    (assoc. 'x '((x NEW) (x a) (y b)))
    (assoc. 'x '((y a) (z b)))
    (assoc. nil '((x a) (y b)))

一个惊喜

首先是这个 eval 函数的参数到底是什么。根据我的理解, (eval. e a) 中的 e 是一个 Lisp 表达式,而 a 则是变量列表。 a 的形式就是 pair 函数返回的那种 ((c1 v1) ... (cn vn)) ,这个变量列表提供给前面的表达式 e 使用。如 (eval. '(eq x y) '((x 1) (y 2))) 就相当于 (eq 1 2) 。这里先把原始代码贴出来,为了方便后面叙述,我把四个子句分别叫做情况1到情况4。

    (defun eval. (e a)
      (cond ((atom e) (assoc. e a))
            ;; 这里是情况2
            ((atom (car e))
             (cond
              ((eq (car e) 'quote) (cadr e))
              ((eq (car e) 'atom) (atom (eval. (cadr e) a)))
              ((eq (car e) 'eq) (eq (eval. (cadr e) a) (eval. (caddr e) a)))
              ((eq (car e) 'car) (car (eval. (cadr e) a)))
              ((eq (car e) 'cdr) (cdr (eval. (cadr e) a)))
              ((eq (car e) 'cons) (cons (eval. (cadr e) a) (eval. (caddr e) a)))
              ((eq (car e) 'cond) (evcon. (cdr e) a))
              ('t (eval. (cons (assoc. (car e) a) (cdr e)) a))))
            ;; 这里是情况3
            ((eq (caar e) 'label)
             (eval. (cons (caddar e) (cdr e)) (cons (list (cadar e) (car e)) a)))
            ;; 这里是情况4
            ((eq (caar e) 'lambda)
             (eval. (caddar e) (append. (pair. (cadar e) (evlis. (cdr e) a)) a)))))
    
    (defun evcon. (c a)
      (cond ((eval. (caar c) a) (eval. (cadar c) a))
            ('t (evcon. (cdr c) a))))
    
    (defun evlis. (m a)
      (cond ((null. m) '())
            ('t (cons (eval. (car m) a) (evlis. (cdr m) a)))))

情况1

情况1是最简单的了。如果 e 是一个原子,就直接到 a 里面寻找相应的值就可以了。

情况2和 evcon. 函数

情况2也不是很复杂,每个原始操作符对应一条语句,其中 cond 用到了一个辅助函数。

  1. 函数 evcon. 是用来辅助计算形如 (eval. '(cond (p1 e1 ... ei) ... (pn en ... ek)) a) 的表达式。(不要在意e的下标,乱写的)
  2. 这个式子最终传给 evcon.c((p1 e1 ... ei) ... (pn en ... ek))a 就还是那个 a 。这个时候 (caar c) 就是 p1 ,所以其实就是调用 eval 来计算 p1 。如果计算结果为真,就调用 eval 计算 e1
  3. 之后就是如果 p1 的计算结果为假,就依次计算 p2, ... pn 直到有一个为真为止。
  4. 如果全部为假,根据定义会变成计算 (evcon. nil a) 。这里会又有一个死循环,因为 (evcon. nil a) 对应的参数 cnil ,此时 (caar c) 也是 nil ,根据对 assoc. 的分析, (eval. (caar c) a) 将会返回 nil ,于是 evcon. 进入第二个分支。但是这个时候 (cdr c) 也是 nil ,于是又变成了 (evcon. nil a) ,变成了死循环。 可以试着执行 (eval. '(cond ((atom '(x)) 'Hello)) nil) , Emacs 会直接报错。想修改这一点的表现也是开头加个判断就好,和修改 assoc. 差不多。
  5. 这里还要提一点就是这个 cond 和现在的 cond 表现是不大一样的,现在的 cond 会把 ei 的计算结果作为返回值,而这个 cond 根本就没有计算 e1 后面的式子。关于这个表现不同,可以试试下面两个式子:
    (eval. '(cond ((atom x) 'Hello 'World)) '((x 1)))
    (cond ((atom 1) 'Hello 'World))

情况4与 evlis. 函数

先分析情况4,之后分析情况3的时候还会用到。情况4用到了一个辅助函数 evlis ,函数 evlis 用来计算 lambda 表达式。

  1. 假设有这个表达式,其中 p1 ... pn 是形参, c1 ... cn 是实参。

    (eval. '((lambda (p1 ... pn) expression) c1 ... cn) a)

    那么有

    (caddar e) 的结果是函数主体 expression

    (cadar e) 的结果是形参列表 (p1 ... pn)

    (cdr e) 的结果是实参列表 (c1 ... cn)

  2. 把这些值代入 evlis. 可得我们实际计算的表达式是:

    (evlis. (c1 ... cn) a)

  3. 分析 evlis. 的定义可知,它最终的返回值是每一个实参的计算结果形成的列表,即返回值为:

    ((eval. c1 a) ... (eval. cn a))

    (eval. cn a)vn

  4. 之后我们回到 eval. 函数, evlis. 返回之后执行的表达式是 (pair. (cadar e) (evlis. (cdr e) a)) 。它的返回结果为

    ((p1 v1) ... (pn vn))

    之后利用 append. 我们成功将形参及其对应的实参值加入了变量列表 a 。把这个新列表记作 New-a

  5. 最后我们就将整个 lambda 表达式转换成了如下形式:

    (eval. expression New-a)

    这样我们就实现了 lambda 语句的解析。

如果被一堆 car,cdr 搞晕了,可以让 Emacs 辅助你分析一下。

    (setq e '((lambda (p1 ... pn) expression) c1 ... cn))
    (caddar e)
    (cadar e)
    (cdr e)

最后可以结合实例试着自己分析下: (eval. '((lambda (x y) (eq x y)) 'x 'x) nil)

情况3

最后是情况3的分析。

  1. 假设有下列表达式:

    (eval. '((label fun (lambda (p1 ... pn) expression)) c1 ... cn) a)

    其中 expression 包含一句形如 (fun q1 ... qn) 的表达式。这里的 (q1 ... qn)(p1 ... pn) 运算得来的。(想想递归的定义方式,这里的 (q1 ... qn) 是传递给下一次递归计算的参数,也就是对 (p1 ... pn) 进行某种运算之后的结果)

  2. 经过情况3对应的语句,表达式变成下列形式:

    (eval. '((lambda (p1 ... pn) experssion) c1 ... cn) New-a)

    这里的 New-a 是原始的 a 中多了一项关于函数名函数本身的关系,即多了下面这一项:

    (fun (label fun (lambda (p1 ... pn) expression)))

  3. 之后根据情况4的分析,表达式继续变形为:

    (eval. expression New-New-a)

    其中 New-New-a 在刚刚的基础上又把形参和对应的实参运算结果对 ((p1 v1) ... (pn ... vn)) 添加了进来,这里的 vn 和情况4相同,还是 (eval. cn New-a)

  4. 于是,整个表达式变成了:

    (eval. '(fun q1 ... qn) New-New-a)

  5. 之后对 fun 再次进行替换,就又变成了:

    (eval. '((label fun (lambda (p1 ...pn) expression)) q1 ... qn) New-New-a)

    可以看到这时表达式又变回了第一步中的形式,但是实参已经是计算过的 (q1 ... qn) 。这样我们就实现了一次迭代。之后重复这个过程,就得到了我们想要的结果。

同样,可以让 Emacs 帮你理下思路

    (setq e '((label fun (lambda (p1 ... pn) expression)) c1 ... cn))
    (caddar e)
    (cdr e)
    (cons (caddar e) (cdr e))
    (list (cadar e) (car e))

后记

读eval真的是太快乐了

本来以为一个晚上就能读完的……还是高估自己了。不知道 Emacs Lisp 怎么 debug,最后还是回归到最原始的方法, print 调试,脑子打断点,纸笔做追踪。不过 Lisp 能 REPL,还是帮脑子分担了不少压力的。

忘了在哪看到的一句话说,使用一门编程语言很大程度上其实是在使用这个编程语言的标准库,但是我们不能单单以标准库的大小来判断一门语言的好坏。当然这并不是说标准库不重要,我自己就挺喜欢 Python 的,除开非常强大的标准库,还有这么多第三方库可以用,非常的方便嘛。但是如果有这么一个语言的核心设计的很简洁,比如 Lisp 定义了几个符号之后就可以写出 eval 函数作为语言的解释器,那么了解一下这门语言也没什么,就当玩了。就像了 Linus 自传的书名一样,“Just for Fun” 。(这本书我还没有读完!)

最初是因为玩 Emacs 才接触到 Lisp 的,中间找了点 Lisp 的教程,看了开头就弃坑了,过了段时间又捡起来看,然后很快又弃坑了……我对 Lisp 的理解基本上局限于基本操作符和函数定义,只能说比 Hello World 的水平强一点。这点理解目前足够我折腾我那个小小的 Emacs 配置文件了,所以还没有什么特别的动力让我继续深入 Lisp ,非要找一个的话就还是那句 Just for Fun。《黑客与画家》这本书里面说 Lisp 非常,非常强大,应该是我水平不到家,还没感觉出来 Lisp 非常强大在哪,也许以后把 Lisp 宏的部分看了才会更有感触吧。

(后面又翻了一下,第二段关于编程语言的核心和标准库的观点好像也是《黑客与画家》这本书里的。不过书里用的词是“内核”和“函数库”)

原文代码

这里附上原文中的代码,省略了 label 的部分,唯一一处修改是那个 funcall

    ;; 原始操作符部分
    ;; quote
    (quote a)
    (quote (a b c))
    
    ;; atom 
    (atom 'a)
    (atom '(a b c))
    (atom '())
    (atom (atom 'a))
    (atom '(atom 'a))
    
    ;; eq
    (eq 'a 'a)
    (eq 'a 'b)
    (eq '() '())
    
    ;; car
    (car '(a b c))
    
    ;; cdr
    (cdr '(a b c))
    
    ;; cons
    (cons 'a '(b c))
    (cons 'a (cons 'b (cons 'c '())))
    (car (cons 'a '(b c)))
    (cdr (cons 'a '(b c)))
    
    ;; cond
    (cond ((eq 'a 'b) 'first)
          ((atom 'a) 'second))
    
    ;; 函数的表示(label部分略,其中一个子句添加了funcall)
    ((lambda (x) (cons x '(b))) 'a)
    
    ((lambda (f) (funcall f '(b c)))
     '(lambda (x) (cons 'a x)))
    
    (defun subst (x y z)
      (cond ((atom z) (cond ((eq z y) x)
                            ('t z)))
            ('t (cons (subst x y (car z))
                      (subst x y (cdr z))))))
    
    (subst 'm 'b '(a b (a b c) d))
    
    ;; 一些函数
    ;; cxr 和 list 
    (cadr '((a b) (c d) e))
    (caddr '((a b) (c d) e))
    (cdar '((a b) (c d) e))
    
    (cons 'a (cons 'b (cons 'c '())))
    (list 'a 'b 'c)
    
    ;; null
    (defun null. (x)
      (eq x '()))
    
    (null. 'a)
    (null. '())
    
    ;;and
    (defun and. (x y)
      (cond (x (cond (y 't) ('t '())))
            ('t '())))
    
    (and. (atom 'a) (eq 'a 'a))
    (and. (atom 'a) (eq 'b 'a))
    
    ;; not
    (defun not. (x)
      (cond (x '())
            ('t 't)))
    
    (not. (eq 'a 'a))
    (not. (eq 'b 'a))
    
    ;; append
    (defun append. (x y)
      (cond ((null. x) y)
            ('t (cons (car x) (append. (cdr x) y)))))
    
    (append. '(a b) '(c d))
    (append. '() '(c d))
    
    ;; pair
    (defun pair. (x y)
      (cond ((and. (null. x) (null. y)) '())
            ((and. (not. (atom x)) (not. (atom y)))
             (cons (list (car x) (car y)) (pair. (cdr x) (cdr y))))))
    
    (pair. '(x y z) '(a b c))
    
    ;; assoc
    (defun assoc. (x y)
      (cond ((eq (caar y) x) (cadar y))
            ('t (assoc. x (cdr y)))))
    
    (assoc. 'x '((x a) (y b)))
    (assoc. 'x '((x new) (x a) (y b)))
    
    ;; 一个惊喜
    (defun eval. (e a)
      (cond ((atom e) (assoc. e a))
            ((atom (car e))
             (cond
              ((eq (car e) 'quote) (cadr e))
              ((eq (car e) 'atom) (atom (eval. (cadr e) a)))
              ((eq (car e) 'eq) (eq (eval. (cadr e) a) (eval. (caddr e) a)))
              ((eq (car e) 'car) (car (eval. (cadr e) a)))
              ((eq (car e) 'cdr) (cdr (eval. (cadr e) a)))
              ((eq (car e) 'cons) (cons (eval. (cadr e) a) (eval. (caddr e) a)))
              ((eq (car e) 'cond) (evcon. (cdr e) a))
              ('t (eval. (cons (assoc. (car e) a) (cdr e)) a))))
            ((eq (caar e) 'label)
             (eval. (cons (caddar e) (cdr e)) (cons (list (cadar e) (car e)) a)))
            ((eq (caar e) 'lambda)
             (eval. (caddar e) (append. (pair. (cadar e) (evlis. (cdr e) a)) a)))))
    
    (defun evcon. (c a)
      (cond ((eval. (caar c) a) (eval. (cadar c) a))
            ('t (evcon. (cdr c) a))))
    
    (defun evlis. (m a)
      (cond ((null. m) '())
            ('t (cons (eval. (car m) a) (evlis. (cdr m) a)))))
    
    ;; 子句1示例
    (eval. 'x '((x a) (y b)))
    
    ;; 子句2示例
    (eval. '(eq 'a 'a) '())
    
    (eval. '(cons x '(b c))
           '((x a) (y b)))
    
    (eval. '(cond ((atom x) 'atom)
                  ('t 'list))
           '((x '(a b))))
    
    ;; 子句2最后部分示例,上面的表达式被替换为下面的
    (eval. '(f '(b c))
           '((f (lambda (x) (cons 'a x)))))
    
    (eval. '((lambda (x) (cons 'a x)) '(b c))
           '((f (lambda (x) (cons 'a x)))))
    
    ;; 子句3示例,label的替换,上面的表达式被替换为下面的
    (eval. '((label firstatom (lambda (x)
                                (cond ((atom x) x)
                                      ('t (firstatom (car x))))))
             y)
           '((y ((a b) (c d)))))
    
    (eval. '((lambda (x)
               (cond ((atom x) x)
                     ('t (firstatom (car x)))))
             y)
           '((firstatom
              (label firstatom (lambda (x)
                                 (cond ((atom x) x)
                                       ('t (firstatom (car x)))))))
             (y ((a b) (c d)))))
    
    ;; 子句4示例,lambda的替换,上面的表达式被替换为下面的
    (eval. '((lambda (x y) (cons x (cdr y)))
             'a
             '(b c d))
           '())
    
    (eval. '(cons x (cdr y))
           '((x a) (y (b c d))))

Footnotes

1 关于这个函数,可以取看看《ANSI Common Lisp》里的函数作为对象或者是《Practical Common Lisp》里面的Functions As Data, a.k.a. Higher-Order Functions。总之加了这个之后代码就能继续运行了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值