McCarthy 1960 年论文的中文翻译

目录

这里 是原文链接

毕竟是 60 年前的文章,当时深奥的概念在现代可能也就小学生水平。我总结了这篇论文里比较吸引人的几点:

  1. 这篇论文 并不是 定义了 LISP 的文章。这篇论文发表之前,LISP 就已经存在于现实中了。不过应该可以把这篇论文中的 LISP 视作最初的 LISP。

  2. 这篇论文中,用于表达数据的是 S-表达式,而用于表达计算的是 M-表达式(还有一种没什么存在感的 L-表达式)。虽说它们之间有一个非常自然的对应关系,但是它们终究还是不同,似乎也导致了刚开始人们并没有发明宏。

  3. “条件判断” 在这篇论文中首创。此前的程序员通过条件跳转指令(或者类似的东西,像是 FORTRAN 中那样)来实现条件判断。McCarthy 可能已经在 FORTRAN 里实现了条件判断的 函数 ,但是无法满足 “只做必要的计算”的需求,所以把条件判断规定为了 LISP 的原语。

  4. “垃圾回收” 也是这篇论文中首次提出的(包括这个名字)。这种方式和列表相性不错,因为能利用上所有碎片化的内存。

  5. 然而更为知名的列表却并不是 LISP 首创的,§7 参考文献 2 中首次提出了列表。但是该论文中并没有提出垃圾回收,其中的内存管理机制为:维护一个“available-space” 列表,需要内存时从中取出一个地址,删去列表元素时将其地址放入 “available-space” 列表中。§7 参考文献 2 似乎没有考虑多个列表共享尾部结构的情况。并且这个操作无法释放元素(element,§7 参考文献 2 中意为 “原子”)所占据的内存。

来自他人的读书笔记:

此外,也推荐阅读 McCarthy 后来编写的 History of Lisp

1. 简介

标题:符号表达式的递归函数及其机器计算,第一部分1 , 2

作者:John McCarthy,麻省理工学院,马萨诸塞州剑桥市。

麻省理工学院的人工智能小组为 IBM 704 计算机开发了一种名为 LISP(表示列表处理器,LISt Processor)的程序系统。该系统旨在促进对拟议的称为“Advice Taker” 的系统的实验,通过该系统,可以指示机器处理陈述句和祈使句,并在执行命令时表现出 “常识”(common sense)。关于 Advice Taker 的最初方案(§7 参考文献 1)在 1958 年 11 月提出。主要要求是建立一个程序系统,用于操作表示形式化陈述句和祈使句的表达式,以便 Advice Taker 系统可以进行推论。

LISP 系统在发展过程中经历了几个简化阶段,最终方案基于表示符号表达式的缺漏递归函数(partial recursive function)。这种表示独立于 IBM 704 计算机(当然也独立于其他电子计算机),所以从所谓 “S-表达式” 和 “S-函数”开始阐述系统应该挺合理。

在本文中,我们首先描述了递归定义函数的形式。我们认为这种形式无论作为编程语言还是计算理论的开发工具都有优势。接下来,我们描述 S-表达式和 S-函数,举一些例子,然后描述泛应用3 S-函数 apply。于理论,它是通用图灵机;于实际,它是 LISP 解释器。接下来,我们通过类似于 Newell、Shaw 和 Simon(§7 参考文献 2)使用的列表结构,在 IBM 704 内存表示 S-表达式,然后用程序表示 S-函数。之后我们提到 IBM 704 上 LISP 程序系统的主要功能。接下来是另一种用符号表达式描述计算的方法。最后我们用递归函数解释了流程图。

我们希望在另一篇论文中描述 LISP 用于的一些符号计算,并在其他地方将我们的递归函数形式应用于数理逻辑和机械定理证明问题。

2. 函数及其定义

我们将需要一些关于一般函数的数学思想和符号。大多数想法都是众所周知的,但条件表达式的概念应该是新的4,我们可以用它以一种新的便利方式定义递归函数。

2.1. 缺漏函数(partial function)

缺漏函数指,只对定义域的一部分定义了返回值的函数。缺漏函数对计算来说是必要的,因为(比方说)计算可能陷入无限递归,这时函数的返回值是未定义的。我们的一些基本函数也会被定义为缺漏函数。

2.2. 命题表达式与谓词(propositional expression, predicate)

命题表达式指返回值只有可能是 T (表示真)或者 F (表示假)的表达式。我们可以在命题表达式中使用常见的数学连词,也就是与或非(∧, ∨, ¬)。典型的命题表达式是:

 x < y                     
(x < y) ∧ (b = c)      
 x 是质数                   

谓词指返回值要么是 T ,要么是 F 的函数。

2.3. 条件表达式(Conditional Expression)

在数学中,真值对其他类型的值的依赖关系用谓词表示,真值对其他真值的依赖关系用逻辑连词表示。然而,表达其他类型的值对真值的依赖关系的数学符号不够多,所以这种关系的表示通常需要用到中文短语。比方说绝对值函数 |x| 就通常用中文来定义。条件表达式是表达一般表达式和命题表达式之间关系的一种工具。条件表达式形如:

(p₁ → e₁, p₂ → e₂, ⋯, pₙ → eₙ)

其中 p₁, p₂, ⋯, pₙ 为命题表达式, e₁, e₂, ⋯, eₙ 为任意表达式。可以读作“如果 p₁ ,那么 e₁ ,否则如果 p₂ ,那么 e₂ ,……,否则如果 pₙ ,那么 eₙ ” 或者 “p₁ 则 e₁ , p₂ 则 e₂ ,……, pₙ 则 eₙ ”5

接下来我们给出条件表达式的求值方式,以及它的值是否有定义的判断方式。从左到右检测 p 的值,如果遇到了值为 T 的 pi ,并且这时还没有遇到未定义的 p ,那么整个条件表达式的值就是对应的 ei (如果有定义);如果在检测到值为 T 的 p 之前就检测到了未定义的 p ,或者所有 p 的值都是 F ,或者 ei 的值未定义,那么该条件表达式的值是未定义的。比方说:

  • (1 < 2 → 4, 1 > 2 → 3) = 4
  • (2 < 1 → 4, 2 > 1 → 3, 2 > 1 → 2) = 3
  • (2 < 1 → 4, T → 3) = 3
  • (2 < 1 → 0/0, T → 3) = 3
  • (2 < 1 → 3, T → 0/0) 的值未定义
  • (2 < 1 → 3, 4 < 1 → 4) 的值未定义

顺便也给一些简单的实际应用。

  • |x| = (x < 0 → -x, T → x)
  • δi, j = (i = j → 1, T → 0)
  • sgn(x) = (x < 0 → -1, x = 0 → 0, T → 1)

2.4. 递归的函数定义

通过条件表达式,我们可以在函数定义体中使用它自己,同时避免无限循环。比方说,

n! = (n = 0 → 1, T → n⋅(n-1)!)

那么我们求值 0! 的结果是 1;因为函数的定义方式,没有意义的 0⋅(0-1)! 求值的结果并不会出现。再比如说 2! 的求值过程是:

2! = (2 = 0 → 1, T → 2 ⋅ (2-1)!)                             
   = 2⋅1!                                                    
   = 2⋅(1 = 0 → 1, t → 1⋅(1-1)!)                     
   = 2⋅1⋅0!                                            
   = 2⋅1⋅(0 = 0 → 1, t → 0⋅(0-1)!)           
   = 2⋅1⋅1                                             
   = 2                                                            

我们再给出其它两个递归函数的定义。第一个是最大公因数(Greatest Common Divisor),我们根据欧几里得辗转相除法可以将它定义为:

gcd(m, n) = (m > n → gcd(n, m), m 整除 n → m, T → gcd(n mod m, m))

其中 n mod m 表示用 m 除 n 后得到的余数。

牛顿迭代法是一种求指定数字平方根估计值的算法。设指定的数字为 a ,初始估计值为 x ,可接受的估计值 y 需要满足 |y2 − a| < ε ,可以写成:

sqrt(a, x, ε) = (|x2 - a| < ε → x, T → sqrt(a, (x + a/x)/2, ε))

也有多个函数组合而成的递归,我们有需要的时候会用到这种递归。

不能保证递归函数一定会终止,比方说上面的 n! 在 n < 0 时就是如此。如果递归不会终止,那么将返回值视作未定义。

逻辑连词也可以用条件表达式定义:

p ∧ q = (p → q, T → F)            
p ∨ q = (p → T, T → q)            
   ¬p = (p → F, T → T)   
p ⊃ q = (p → q, T → T)

如果我们考虑未定义的值,那么 ∧ 和 ∨ 并不是可交换运算。假如 p 是 F , q 是未定义,那么根据上面的定义 p ∧ q 是 F ,但是 q ∧ p 却是未定义。对于我们的应用领域来说这种行为其实是应该的,因为 p ∧ q 求值时首先会求值 p ,如果它为 F 那么 q 不会被求值。如果 p 的计算没有终止,那么我们永远没有办法计算 q 。我们今后以此种意味使用逻辑连词。

2.5. 函数、形式(function, form)

函数通常用于数学中,用于指代 y2 + x 这样的形式。但是我们接下来的计算会涉及 “求值为函数的表达式”,所以我们需要区分函数和形式,并且需要一种记法来表达出它们的差异。我们选用了丘奇(Church,λ 演算发明人)的方式。

令 f 为一个表达式,其表示一个函数,有刚好两个整数参数。那么显然,我们应该能写下表达式 f(3, 4) ,并且这个表达式的值是确定的。 y2 + x 并不算函数,因为 y2 + x(3, 4) 不像是什么经常见到的记法。就算真的要用这种记法的话,我们也不能知道它到底是 13 还是 19。丘奇把 y2 + x 这样的东西叫做 “形式(form)”,形式只有在可以确定其中出现的变量的顺序时才可以转化为函数。这一步骤通过丘奇的 λ 表示法(§7 参考文献 3)完成。

令 ε 为一个形式,其内变量为 x₁, x₂, ⋯, xₙ ,那么 λ((x₁, ⋯, xₙ), ε) 就表示有 n 个参数的函数,返回值为 x₁, ⋯, xₙ 对应替换成各个参数后,形式 ε 的值。比方说, λ((x, y), y2 + x) 是两个参数的函数, λ((x, y), y2 + x)(3, 4) = 19 。

λ 表达式中出现的变量是 “被绑定的”(bound),所以我们可以改变变量的名称,而不会改变表达式的值(当然不能重名)。也就是说 λ((x, y), y2 + x),λ((u, v), v2 + u), λ((y, x), x2 + y) 都是同一个函数。

我们之后会频繁用到这样一种表达式,其中有一些变量出现在 λ 中,而另一些没有。这样的表达式可能被视作带 parameter 6的函数。我们把这些没有绑定的变量称作 “自由变量”。

可以区分形式和函数的记法都可以表示出函数的函数。这里再给例子就容易离题了,不过我们接下来需要用到以函数作为参数的函数。

2.6. 表达式和递归函数

λ 表示法不足以表示递归函数7。比方说,我们可以用 λ 把:

sqrt(a, x, ε) = (|x2 - a| < ε → x, T → sqrt(a, (x + a/x)/2, ε))

转换成:

sqrt = λ((a, x, ε), (|x2 - a| < ε → x, T → sqrt(a, (x + a/x)/2, ε)))

但是等号右侧并不能用作表达式,因为没有办法让 sqrt 表示外层的整个 λ 表达式。

为了编写求值为递归函数的表达式,我们引入一条新的记法。 label(a, ε) 基本上就与表达式 ε 等价,但是其中 a 的出现会被解释为对整个 ε 的引用。因此我们可以编写:

label(sqrt, λ((a, x, ε), (|x2 - a| < ε → x, T → sqrt(a, (x + a/x)/2, ε))))

label(a, ε) 中的符号 a 也是被绑定的,所以替换掉所有的 a 并不会影响表达式的值。不过 label(a, ε) 中 a 的行为和 λ 绑定的变量稍微有些不同就是了。

3. 符号表达式的递归函数

我们首先要定义符号表达式,其中需要用到有序点对和列表。接下来我们定义 5 个基本的函数和谓词,通过组合、条件表达式以及递归构建广阔领域内的函数,并提供大量示例。接下来我们会展示条件表达式以及递归函数如何记作符号表达式,并定义泛应用函数 apply,从而计算从表达式获得的函数在不定数量的参数下的值。最后,我们会定义一些以函数作为参数的函数,并给出一些实用示例。

3.1. 符号表达式

我们现在定义 S-表达式(S 表示 “符号的”,Symbolic)。其由特殊字符:

⋅ ( )

以及一组无穷个符号构成。其中符号是原子,也就是不能继续分成更细的组件。至于符号的表示,我们使用大写拉丁字母、十个数字和单词间空格所构成的字符串。8比方说以下这些都是符号:

  • A
  • ABA
  • APPLE PIE NUMBER 3

不使用类似数学的单个字母有两个原因。第一,计算机程序常常需要数百个可以互相区分的符号,并且需要用 IBM 704 可以显示的 47 个字符表示。第二,用英文单词和短语作为原子比较方便记。因为符号是原子,所以它(作为字符串)的所有子结构都被忽略。我们认为,名字不同的符号是不同的,名字相同的符号是相同的。接下来我们定义 S-表达式:

  1. 符号是 S-表达式
  2. 如果 e₁ 和 e₂ 都是 S-表达式,那么 (e₁ ⋅ e₂) 也是。

举个例子,以下这些都是 S-表达式:

  • AB
  • (A ⋅ B)
  • ((AB ⋅ C) ⋅ D)

也就是说,S-表达式是有序的点对结构,各部分均为更简单的 S-表达式或者符号。我们可以用 S-表达式来表示任意长度的列表。给定 n 个元素的列表:

(m₁, m₂, ⋯, mₙ)

我们用这样的 S-表达式来表示它:

(m₁ ⋅ (m₂ ⋅ (⋯ (mₙ ⋅ NIL)⋯)))

其中 NIL 是一个符号,用于表示列表的终止。因为我们要处理的大部分 S-表达式都可以方便地表示为列表,所以我们接下来引入一种特定 S-表达式的缩写:

  1. (m) 表示 (m ⋅ NIL)
  2. (m₁, ⋯, mₙ) 表示 (m₁ ⋅ (⋯ (mₙ ⋅ NIL)⋯))
  3. (m₁, ⋯, mₙ ⋅ x) 表示 (m₁ ⋅ (⋯ (mₙ ⋅ x)⋯))

子表达式可以用类似的方法简化。举两个例子:

  • ((AB, C), D) 表示 ((AB ⋅ (C ⋅ NIL)) ⋅ (D ⋅ NIL))
  • ((A, B), C, D ⋅ E) 表示 ((A ⋅ (B ⋅ NIL)) ⋅ (C ⋅ (D ⋅ E)))

因为我们把带逗号的表达式视作不带逗号的表达式的缩写,所以我们把它们全都称作 S-表达式。

3.2. S-表达式的函数以及表示它们的表达式

我们接下来定义一类 S-表达式的函数。表示这些函数的表达式是用常规函数符号编写的。但是,为了清楚地区分表示函数的表达式和 S-表达式,我们将使用小写字母序列来表示函数名称和变量,其中变量可以用于 S 表达式中。我们还会在涉及函数时使用方括号和分号,而不是圆括号和逗号。因此以下是合法的函数调用:

  • car[​x]
  • car[cons[(A ⋅ B); x]]

这些 M-表达式(Meta-expression)中所有 S-表达式都表示它们自己。

3.3. 基本 S-函数和 S-谓词

我们引入以下函数和谓词:

  1. atom: atom[​x] 的值是 T 或者 F ,具体取决于 x 是否是原子。因此

    • atom[​X] = T
    • atom[(X ⋅ A)] = F
  2. eq: eq[x; y] 只有在 x 和 y 都是符号时才有定义。如果它们是相同的符号,那么 eq[x; y] = T;否则 eq[x; y] = F 。

  3. car: car[​x] 只有在 x 不是原子时才有定义。我们定义 car[(e₁ ⋅ e₂)] 为 e₁ ,因此

    • car[​X] 未定义
    • car[(X ⋅ A)] = X
    • car[((X ⋅ A) ⋅ Y)] = (X ⋅ A)
  4. cdr: cdr[​x] 同样在 x 不是原子时才有定义。我们定义 cdr[(e₁ ⋅ e₂)] 为 e₂ ,因此

    • cdr[​X] 未定义
    • cdr[(X ⋅ A)] = A
    • cdr[((X ⋅ A) ⋅ Y)] = Y
  5. cons: cons[x; y] 对任何 x, y 都有定义。我们定义 cons[e₁; e₂] 为 (e₁ ⋅ e₂) ,因此:

    cons[X; A] = (X ⋅ A) cons[(X ⋅ A); Y] = ((X ⋅ A) ⋅ Y)

    容易发现 car, cdr 和 cons 之间的关系。

    • car[cons[x; y]] = x
    • car[cons[x; y]] = y
    • car[car[​x]; cdr[​x]] = x (其中 x 不能是原子)

    等之后我们讨论了该系统在计算机中的表示,“car” 和 “cdr” 这两个名称就会有助记符意义了。组合 car 和 cdr 可以获取表达式在指定位置的子表达式。组合 cons 可以用子表达式构造出指定结构的表达式。以这种方式形成的函数非常有限,没有研究价值。

3.4. 递归 S-函数

通过条件表达式和递归函数定义,我们可以得到多得多的函数(实际上,是所有可计算函数)。我们现在给出可以这样定义的几个函数。

  1. ff[​x] 的返回值是 S-表达式 x 中的第一个原子,忽略括号。比方说

    ff[((A ⋅ B) ⋅ C)] = A

    我们定义

    ff[​x] = [atom[​x] → x; T → ff[car[​x]]]

    接下来我们跟踪 ff[((A ⋅ B) ⋅ C)] 求值的每一步:

      ff[((A ⋅ B) ⋅ C)]                                    
    = [atom[((A ⋅ B) ⋅ C)] → ((A ⋅ B) ⋅ C); T → ff[car[((A ⋅ B) ⋅ C)]]]    
    = [F → ((A ⋅ B) ⋅ C); T → ff[car[((A ⋅ B) ⋅ C)]]]                 
    = [T → ff[car[((A ⋅ B) ⋅ C)]]]                                   
    = ff[car[((A ⋅ B) ⋅ C)]]                                         
    = ff[(A ⋅ B)]                                                   
    = [atom[(A ⋅ B)] → (A ⋅ B); T → ff[car[(A ⋅ B)]]]               
    = [F → (A ⋅ B); T → ff[car[(A ⋅ B)]]]                          
    = [T → ff[car[(A ⋅ B)]]]     
    = ff[car[(A ⋅ B)]]             
    = ff[A]                            
    = [atom[A] → A; T → ff[car[A]]]
    = [T → A; T → ff[car[A]]]      
    = A                                            
    
    1. subst[x; y; z] 把 S-表达式 z 中出现的所有符号 y 替换为 S-表达式 x。其定义为:

      subst[x; y; z] = [atom[z] → [eq[z; y] → x; T → z];                      
                              T → cons[subst[x; y; car[z]]; subst[x; y; cdr[z]]]]
      

      举个例子,可以求出:

      subst[(X ⋅ A); B; ((A ⋅ B) ⋅ C)] = ((A ⋅ (X ⋅ A)) ⋅ C)

  2. equal[x; y] 是一个谓词,它在两参数为相同 S-表达式时返回 T,其他情况返回 F。我们定义:

    equal[x; y] = [atom[​x] ∧ atom[y] ∧ eq[x; y]]                    
                ∨ [¬atom[​x] ∧ ¬atom[y]                        
                   ∧ equal[car[​x]; car[y]] ∧ equal[cdr[​x]; cdr[y]]]
    

    用 S-表达式的列表缩写法可以方便地看到基本函数长什么样。容易验证:

  3. car[(m₁, m₂, ⋯ , mₙ)] = m₁

  4. cdr[(ms, m₂, ⋯ , mₙ)] = (m₂, ⋯ , mₙ)

  5. cdr[(m)] = NIL

  6. cons[m₁; (m₂, ⋯ , mₙ)] = (m₁, m₂, ⋯ , mₙ)

  7. cons[m; NIL] = (m)

我们定义

null[​x] = atom[​x] ∧ eq[x; NIL]

这个谓词在处理列表时很有用。

因为我们经常要组合 car 和 cdr ,所以我们定义如下的缩写:

  • cadr[​x] 表示 car[cdr[​x]]
  • caddr[​x] 表示 car[cdr[cdr[​x]]] ,以此类推

另一个有用的缩写是

list[e₁; e₂; ⋯; eₙ] = cons[e₁; cons[e₂; ⋯ cons[eₙ; NIL]⋯]]

这个函数把所有参数组合成一个列表。

把 S-表达式视作列表时,下面这几个函数会很常用。

  1. append[x; y]

    append[x; y] = [null[​x] → y; T → cons[car[​X]; append[cdr[​X], y]]]

    例如, append[(A, B); (C, D, E)] = (A, B, C, D, E)

  2. among[x; y]

    该谓词判断 S-表达式是否出现于列表 y 之中,其定义为:

    among[x; y] = ¬null[y] ∧ [equal[x; car[y]] ∨ among[x; cdr[y]]]

  3. pairs[x; y]

    该函数返回二元列表的列表,其中各个元素对应于列表 x 和 y 的元素。

    pairs[x; y] = [ null[​x] ∧ null[y] → NIL; ¬atom[​x] ∧ ¬atom[y] → cons[list[car[​x]; car[y]]; pair[cdr[​x]; cdr[y]]]]

    例如, pair[(A, B, C); (X, (Y, Z), U)] = ((A, X), (B, (Y, Z)), (C, U)) 。

  4. assoc[x; y]

    如果 y 是列表 ((u₁, v₁), (u₂, v₂), …, (uₙ, vₙ)) ,并且 x 是某一个 u ,那么 assoc[x; y] 是对应的 v 。我们定义:

    assoc[x; y] = eq[caar[y]; x] → cadar[y];        
                               T → assoc[x; cdr[y]]
    

    例如,

    assoc[X; ((W, (A, B)),               
              (X, (C, D)),               
              (Y, (E, F)))] = (C, D)
    
  5. sublis[x; y]

    其中 x 形如 ((u₁, v₁), ⋯, (uₙ, vₙ)) ,各个 u 都是符号;y 可以是任意 S-表达式。 sublis[x; y] 会把 y 中所有 u 替换为对应的 v 。为了定义这个函数,我们首先定义一个辅助函数:

    sub2[x; z] = [       null[​x] → z;                
                  eq[caar[​x]; z] → cadar[​x];         
                               T → sub2[cdr[​x]; z]]
    

    接下来我们定义:

    sublis[x; y] = [atom[y] → sub2[x; y];               
                          T → cons[sublis[x; car[y]]; sublis[x; cdr[y]]]]
    

    举个例子, sublis[((X, (A, B)), (Y, (B, C))); (A, X ⋅ Y)] = (A, (A, B), B, C)

3.5. 用 S-表达式表示 S-函数

截至目前,S-函数可以用 M-表达式描述。接下来我们给出一种将 M-表达式转换为 S-表达式的规则,以便能够使用 S-函数对 S-函数进行某些计算,进而回答有关 S-函数的某些问题。

转换由以下规则确定,我们用 ε* 表示 M-表达式 ε 转换成 S-表达式的结果。

  1. 如果 ε 是 S-表达式,那么 ε* = (QUOTE, ε)
  2. 以小写字母表示的函数名和变量名会被转换成大写。因此 car* = CAR , subst* = SUBST 。
  3. 式 f[e₁; e₂; ⋯; eₙ] 会转换为 (f*, e₁*, e₂*, ⋯, eₙ*) 。因此 cons[car[​x]; cdr[​x]]* = (CONS, (CAR X), (CDR X)) 。
  4. [p₁ → e₁; ⋯ ; pₙ → eₙ]* = (COND, (p₁*, e₁*), ⋯ , (pₙ*, eₙ*))
  5. λ[[x₁; ⋯ ; xₙ]; E]* = (LAMBDA, (x₁*, ⋯ , xₙ*), E*)
  6. label[a; E]* = (LABEL, a*, E*)

按照上面这些转换步骤,我们可以把 M-表达式

label[subst; λ[[x; y; z];                                    
               [atom[z] → [eq[y; z] → x; T → z];     
                      T → cons[subst[x; y; car[z]];    
                               subst[x; y; cdr[z]]]]]]

转换成 S-表达式:

(LABEL, SUBST,                                                    
  (LAMBDA, (X, Y, Z),                                             
    (COND, ((ATOM, Z),  (COND, ((EQ, Y, Z), X), ((QUOTE, T), Z))),
           ((QUOTE, T), (CONS, (SUBST, X, Y, (CAR, Z)),
                               (SUBST, X, Y, (CDR, Z)))))))

这种记法是可写的,并且还算稍微有点可读性。如果牺牲结构正规性的话,读写会方便一点。如果电脑上能用的字符更多,那么读写会方便不少。9

3.6. 泛应用函数 apply

令 f’ 是一个 S-函数, f 是其对应的 S-表达式, args 是参数列表,形如 (arg₁, arg₂, ⋯, argₙ) ,其中各个 arg 都是任意 S-表达式。 apply 这个 S-函数满足等式:

apply[f; args] = f’[arg₁; arg₂; ⋯; argₙ]

也就是说,等号左边有定义当且仅当等号右边同样有定义,并且在有定义时它们是相等的。比方说

  λ[[x; y]; cons[car[​x]; y]][(A, B); (C, D)]                 
= apply[(LAMBDA, (X, Y), (CONS, (CAR, X), Y));                    
        ((A, B), (C, D))]                                         
= (A, C, D)

接下来我们给出 apply 的定义:

apply[f; args] = eval[cons[f; appq[args]]; NIL]

其中

appq[m] = [null[m] → NIL;                               
                 T → cons[list[QUOTE; car[m]];          
                          appq[cdr[m]]]]                        

而更重要的 eval 函数的定义如下:

val[e; a]                                                         
  = [atom[e] → assoc[e; a];                                  
     atom[car[e]]
       → [eq[car[e]; QUOTE] → cadr[e];                         
          eq[car[e]; ATOM]  → atom[eval[cadr[e]; a]];            
          eq[car[e]; EQ]    → [eval[cadr[e]; a] = eval[caddr[e]; a]];
          eq[car[e]; COND]  → evcon[cdr[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]];
                          T → eval[cons[assoc[car[e]; a];        
                                        evlis[cdr[e]; a]];          
                                   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]]]

其中涉及了两个小函数:

evcon[c; a] = [eval[caar[c]; a] → eval[cadar[c]; a];   
                              T → evcon[cdr[c]; a]]

evlis[m; a] = [null[m] → NIL;                          
                     T → cons[eval[car[m]; a];         
                              evlis[cdr[m]; a]]]

接下来我们详细解释这些函数定义的几个要点。10

  1. 最顶层的 apply 生成一个表达式,表示 “将给定参数应用于给定函数”,并将计算该表达式的工作转移至函数 eval 。它使用 appq 在每个参数周围加上引号,以便 eval 将它们视为常量。

  2. eval[e; a] 有两个参数,一个要计算的表达式 e ,以及一个二元列表的列表 a 。每个二元列表的第一项是符号,第二项是该符号所代表的 S-表达式。

  3. 如果要计算的表达式是一个原子,也就是符号,则 eval 返回列表 a 中最先与它匹配的内容。

  4. 如果 e 并不是原子,但是 car[e] 是原子,那么表达式必定形如:

    • (QUOTE, e)
    • (ATOM, e)
    • (EQ, e₁, e₂)
    • (COND, (p₁, e₁), ⋯, (pₙ, eₙ))
    • (CAR, e)
    • (CDR, e)
    • (CONS, e₁, e₂)
    • (f, e₁, …, eₙ), 其中 f 是一个符号

    在 (QUOTE, e) 的情况下,直接返回表达式 e 本身。在 (ATOM, e) 或 (CAR, e) 或 (CDR, e) 的情况下,计算表达式 e 并调用适当的函数。如果是 (EQ, e₁, e₂) 或 (CONS, e₁, e₂) ,则需要计算两个表达式。在 (COND, (p₁, e₁), ⋯, (pₙ, eₙ)) 的情况下,按顺序计算 p ,直到找到值为 T 的 p ,然后计算对应的 e 。这是由另一个函数 evcon 完成的。最后,在 (f, e₁, ⋯, eₙ) 的情况下,我们把 f 替换为其在关联表 a 中对应的值,并求值替换后得到的表达式。

  5. 求值 ((LABEL, f, ε), e₁, ⋯, eₙ) 时,我们将 (f, (LABEL, f, ε)) 放在列表 a 的第一个位置,然后求值 (ε, e₁, ⋯, eₙ) 。

  6. 最后,求值 ((LAMBDA, (x₁, ⋯, xₙ), ε), e₁, ⋯, eₙ) 时,我们把二元列表的列表11 ((x₁, e₁), ⋯, (xₙ, eₙ)) 拼接到 a 前面,然后求值 ε 。

其实我们也可以不用列表 a ,用参数替换表达式 ε 中变量来求值 LAMBDA 和 LABEL 表达式。可惜,有多个重名的绑定变量时这个操作会非常麻烦,但是只要使用列表 a 就可以绕过这些问题。

使用 apply 计算函数值不适合人类,而适合计算机。但是为了说明它的原理,我们现在给出下面表达式的计算步骤:

apply[(LABEL, FF,                                                  
        (LAMBDA, (X),                                           
          (COND, ((ATOM, X),  X),                  
                 ((QUOTE, T), (FF, (CAR, X))))));  
      ((A ⋅ B))] = A                                           

第一个参数表示我们在 §3.4 节定义的函数 ff 。我们接下来把它简写成 ɸ 。那么我们有:

  apply[ɸ; ((A ⋅ B))]                                     
= eval[((LABEL, FF, ψ),                                           
        (QUOTE, (A ⋅ B)));                                    
       NIL]

(其中 ψ 是 ɸ 中 “ (LAMBDA ⋯) ” 部分)

= eval[((LAMBDA, (X), ω),                         
        (QUOTE, (A ⋅ B)));     
       ((FF, ɸ))]

(其中 ω 是 ψ 中 “ (COND ⋯) ” 部分)

= eval[(COND, (π₁, ε₁), (π₂, ε₂));    
       ((X,  (QUOTE, (A ⋅ B))),                                 
        (FF, ɸ))]                                             

用 a 表示 ((X, (QUOTE, (A ⋅ B))), (FF, ɸ)) ,我们得到:

= evcon[((π₁, ε₁), (π₂, ε₂)); a]

这会涉及到 eval[π₁; a] ,我们把它展开:

  eval[π₁; a]                                           
= eval[(ATOM, X); a]                                         
= atom[eval[X; a]]                                           
= atom[eval[assoc[X; ((X,  (QUOTE, (A ⋅ B))), (FF, ɸ))];
            a]]                                    
= atom[eval[(QUOTE, (A ⋅ B)); a]]                        
= atom[(A ⋅ B)]                                          
= F                                                          

回到我们的主计算里来,我们现在知道了:

  evcon[((π₁, ε₁), (π₂, ε₂)); a]      
= evcon[((π₂, ε₂)); a]

这一步又涉及到了 eval[π₂; a] = eval[(QUOTE, T); a] = T 。

= eval[(FF, (CAR, X)); a]                                           
= eval[cons[ɸ; evlis[((CAR, X)); a]; a]]                         

其中的 evlis[((CAR, X)); a] 求值时会涉及到:

  eval[(CAR, X); a]                          
= car[eval[X; a]]                                         
= car[(A ⋅ B)]    (这里我们使用了之前 atom[eval[X; a]] 的计算过程)
= A                                                       

因此

evlis[((CAR, X)); a] = list[list[QUOTE, A]] = ((QUOTE, A))

进而,我们的主计算过程可以变为:

  eval[cons[ɸ; evlis[((CAR, X)); a]; a]]                      
= eval[(ɸ, (QUOTE, A)); a]

接下来的计算步骤就跟刚开始时差不多。 LABEL 和 LAMBDA 会向 a 中添加新的二元列表,设结果为 a₁ 。在 cond 表达式中的 π₁ 求值的结果为 eval[π₁; a₁] = eval[(ATOM, X); a₁] = T ,因为在 a₁ 中 X 关联的值为 (QUOTE, A) ,和 a 中不同,在 a 中 X 关联的值是 (QUOTE, (A ⋅ B)) 。

所以 evcon 返回 eval[X; a₁] = A 。

3.7. 以函数作为参数的函数

容易定义出很多以函数作为参数的实用函数。特别是要定义其它函数时,以函数作为参数的函数会更加有用。比方说 maplist[x; f] 这个函数12,其中 x 是一个 S-表达式,而参数 f 是一个单参 S-函数。它的定义是:

maplist[x; f] = [null[​x] → NIL; T → cons[f[​x]; maplist[cdr[​x]; f]]]

接下来,我们编写一个求多元多项式的偏导数的程序,以此说明 maplist 的有用性。我们将要求导的 S-表达式需满足的条件如下。

  1. 任意符号都是允许的表达式。

  2. 如果 e₁, e₂, …, eₙ 是允许的表达式,那么

    • (PLUS, e₁, e₂, ⋯, eₙ)
    • (TIMES, e₁, e₂, ⋯, eₙ)

    都是允许的表达式,分别表示和与积。

其实这本质上就是使用前缀表达式的加法和乘法,不同之处在于使用逗号和括号,并且每个运算都可以使用任意个参数。比方说,这个 S-表达式:

(TIMES, X, (PLUS, X, A), Y)

是我们的程序允许的。用常规的代数表示法为:

X(X + A)Y

接下来是偏导公式的定义,其返回多项式 y 对变量 x 的偏导13

diff[y; x]                                                          
  = [atom[y] → [eq[y; x] → ONE;                                 
                       T → ZERO];                                 
     eq[car[Y], PLUS]                                               
       → cons[PLUS; maplist[cdr[y]; λ[[z]; diff[car[z]; x]]]];
     eq[car[Y], TIMES]                                              
       → cons[PLUS;                                               
              maplist[cdr[y];                                       
                      λ[[z];                                  
                        cons[TIMES;                                 
                             maplist[cdr[y];                        
                                     λ[[w];                   
                                       [¬eq[z; w] → car[w];  
                                                T → diff[car[w]; x]]]]]]]]]

表达式 (TIMES, X, (PLUS, X, A), Y) 对自变量 X 的导数,也就是这个函数的返回值,为

(PLUS, (TIMES, ONE, (PLUS, X, A), Y),                              
       (TIMES, X, (PLUS, ONE, ZERO), Y),                           
       (TIMES, X, (PLUS, X, A), ZERO))

除了上面这个 maplist ,这里再给出一个以函数为参数的实用函数 search :

search[x; p; f; u] = [null[​x] → u;                               
                         p[​x] → f[​x];                            
                            T → search[cdr[​x]; p; f; u]]         

这个函数在列表 x 中搜索拥有属性 p 的元素,如果有,那么返回它在 f 下的像;如果这样的元素不存在,那么返回 u。

脚注

1 原文脚注:这篇论文的 LATEX 化有一部分来自 ARPA(ONR) 给斯坦福大学的资助 N00014-94-1-0775,John McCarthy 自 1962 年以来一直在那里工作。该文章从 1960 年 4 月的 CACM(译注:Communication of the Association for Computing Machinery)复制,稍微更改了一些符号。如果您想要准确的排版,请查看那里。现地址,John McCarthy,计算机科学系,斯坦福,CA 94305,(电子邮件:[email protected]),(网址:http://www-formal.stanford.edu/jmc/

2 译注:实际上并没有 “第二部分”。这篇论文标题里有 “第一部分” 是因为 McCarthy 认为需要 “在另一篇论文中描述 LISP 用于的一些符号计算”。

3 译注:原文为 universal。我将其译为 “泛应用” 是因为这个函数把参数传递给另一个函数,并返回后者返回的值,这一操作通常叫做 “应用”(apply)。而 “泛” 指 apply 接受所有参数组合而成的列表,与之相反的 funcall 就应该叫做 “狭应用”。

4 原文脚注:参考 Kleene

5 原文脚注:我向 CACM 发送了一份关于 Algol 60 中应包含哪些内容的条件表达式提案。由于该项目很短,编辑将其降级为给编辑的一封信,CACM 随后对此道歉。这里给出的符号被 Algol 60 拒绝了,因为它已经决定不使用新的数学符号,新的符号都需要用英语。Algol 60 所采用的 if … then … else 语法由 John Backus 提出。

6 译注:反正这个 parameter 后面也没用到,我就不翻译了。

7 其实是有办法的,也就是 Y 组合子,但是 McCarthy 发表该论文时 Y 组合子还没有被发现。

Y = (λ(f), (λ(x), f(x(x)))((λ(x), f(x(x)))))

或者把 Y 的参数挪到左边:

Y(f) = (λ(x), f(x(x)))((λ(x), f(x(x))))

它有一个重要的性质。令 f 是任意函数,那么:

Y(f) = (λ(x), f(x(x)))((λ(x), f(x(x))))
     = f((λ(x), f(x(x)))((λ(x), f(x(x)))))
     = f(Y(f))

现代数学语言称 Y(f) 为 f 的 “不动点”,因为其经过 f 映射后不变。接下来我们可以用 Y 组合子来定义递归函数。比方说,阶乘函数可以定义为:

Y(λ((factorial),                                             
    (λ((n),                                            
       (n = 0 → 1,                                  
            T → n ⋅ factorial(n - 1))))))

8 原文脚注:1995 年评论:单词间空格之所以允许,是因为列表的元素之间用逗号分隔。

9 原文脚注:1995: SAIL 上可用的字符更多,不久后 Lisp 机上也有了。哎,世界又跌回劣等字符集了——虽然没有跌到这篇论文完成时,1959 年早期。

10 原文脚注:1995:这个版本并不完全正确。这个和其它版本的 eval,包括真正实现了的(并且调试过的),之间的差距见 Herbert Stoyan 的 “The Influence of the Designer and the Design” 以及 Vladimir Lifschitz (ed.) 于 1991 年的 《Artificial Intelligence and Mathematical Theory of Computation: Papers in Honor of John McCarthy》(标题大意:人工智能与计算的数学理论:John McCarhy 精神引领的论文), Academic Press。

11 译注:此处的 e₁, ⋯, eₙ 应该替换为它们求值后的结果。后面的计算过程中,第三个等号有类似的问题。不过在 eval[π₁; a] 的求值过程中第三个等号又多了一个 eval ,所以最终结果并没有出现问题。

12 译注:现代 LISP 中 maplist 的参数顺序和这里相反。此外,M-表达式并不允许参数出现函数,只能出现变量、S-表达式和其它 M-表达式。不过反正 M-表达式也没有用在计算机上,不严谨也没人在乎。

13 译注:这个函数定义有误。第一个问题在于其 eq[car[Y], TIMES] 子句中 eq,它应该改成 equal,因为 eq 在参数不是原子时返回值是未定义的(见§3.3 节)。第二个问题更难发现,请看这个子句:

eq[car[Y], PLUS] → cons[PLUS; maplist[cdr[y]; λ[[z]; diff[car[z]; x]]]]

其中用到了 x,而 x 实际上并不能表示 diff[y; x] 中的 x,因为这个变量被 maplist 覆盖了。这是词法作用域必要性的一个重要论据,Paul Graham 就在其短文《The Roots of Lisp》中提到了该 bug。此外第 3 子句中也有类似的 bug。

14 译注:Advice Taker 是 McCarthy 在他 1959 年论文《Programs with Common Sense》中提出的概念。该论文描述的假想程序可以看作世界上第一个完整的人工智能系统。

15 译注:图 1 可能在历史长河中丢失了 (a) 和 (b) 标记。我猜上方为 1a,左下方为 1b,右下方为 1c。

16 译注:IBM 704 中有两种指令类型:A 型和 B 型。

  • A 型:由 3 比特 “前缀”、15 比特 “减量”、3 比特 “标签” 和 15 比特“地址” 组成。
  • B 型:这里用不到。

https://bitsavers.org/pdf/ibm/704/24-6661-2_704_Manual_1955.pdf

17 译注:原文为关联表(Association Lists),但是根据原文的脚注 6,后来人们称之为属性表(property list)而不是关联表,所以这里将它译作属性表。

18 原文脚注:我们已经给这个步骤命名为 “垃圾回收”(Garbage Collect)了,但是我不敢在这篇论文里用这个名字——Research Laboratory of Electronics(译注:麻省理工学院的一个实验室)的语法女士们不给我用。

19 译注:“累加器” 指可以存储运算结果的寄存器。现代计算机上所有寄存器都是累加器(甚至内存也是),但是计算机刚出现那会儿累加器很少,比方说 Intel 8080 微处理器就只有一个累加器。

20 译注:然而并没有。我觉着这里的 “例外情况” 指的是间接递归,那样的话用这里的方法(用栈保存临时变量)同样能处理。如果 “例外情况” 指的是尾递归或尾调用的话,那么不需要用栈,直接覆盖可用内存就可以。

21 译注:原文为 “公共后进先出列表”(public push-down list),但是根据原文脚注 8,这种数据结构其实就是栈(stack)。

22 译注:指打孔纸带。它在 1960 年的作用就像磁盘在现代的作用,不用通电也能保留数据的存储器。

5 个赞

4. LISP 程序系统

LISP 程序系统是使用 IBM 704 计算机以 S-表达式计算符号信息的系统。它已经,或者即将用于以下目的:

  1. 将 LISP 程序编译成机器语言的编译器。
  2. 检查某种类型的形式逻辑系统中论证的正确性。
  3. 编写形式微分和积分的程序。
  4. 生成谓词演算系统中的证明,需要实现不止一种算法。
  5. 进行某些工程计算,其结果是公式而不是数字。
  6. Advice Taker 14系统的编程。

该系统的基础是编写计算机程序来求值 S-函数的方法。这将在以下部分中描述。

除了描述 S-函数的工具之外,还有一些工具让我们可以在 FORTRAN(§7 参考文献 4)或 ALGOL(§7 参考文献 5)程序中使用 S-函数。本文将不介绍这些功能。

4.1. 用列表结构表示 S-表达式

列表结构是按图 1a 或 1b 15排列的机器字集合。列表结构占用的每个字在图中都使用被分成两块的大矩形表示。大矩形中靠左的小矩形表示 “地址” 字段,靠右的小矩形表示 “减量” 字段16。如果有从小矩形到大矩形的箭头,那么小矩形表示的字段的内容是大矩形对应的字的地址。

允许同一子结构出现在列表结构中的多个位置,如图 1b 所示;但不允许结构具有循环,如图 1c 所示。符号在计算机中用一种特殊形式的列表表示,我们称它为该符号的属性表17。符号的第一个字的地址字段为某个特殊常量,程序能够通过该常量判断该字表示了符号。我们将在 §4.2 节中详细描述属性表。

非原子的 S-表达式 x 由一个字表示,其地址和减量字段分别包含子表达式 car[​x] 和 cdr[​x] 的位置。如果我们使用符号 A、B 等来表示它们各自的关联表的位置,则 S-表达式 ((A ⋅ B) ⋅ (C ⋅ (E ⋅ F))) 由图 2 的列表结构 a 表示。接下来看 S-表达式的列表形式,我们知道 S-表达式 (A, (B, C), D) 等价于 (A ⋅ ((B ⋅ (C ⋅ NIL)) ⋅ (D ⋅ NIL))) ,因此由图 2b 的列表结构表示。

如果把列表结构看作列表,那么不难发现列表的每个元素都占据了一个机器字的地址字段,而该机器字的减量字段指向下一个元素所在的字。最后一个字的减量字段为 NIL 。

子表达式重复出现的表达式可以用多种方式表示。子表达式的列表结构是否重复取决于程序的历史记录。无论以哪种方式表示重复的子表达式,程序结果都不会改变,因为它们都不需要机器就能存在,虽然还是会影响时间和内存的需求。例如,S-表达式 ((A ⋅ B) ⋅ (A ⋅ B)) 可以用图 3a 或图 3b 的列表结构表示。

禁止循环列表结果本质上就是在禁止表达式包含自己。在我们拓扑结构的世界里,这样的表达式不可能存在于纸上。循环列表结构在机器中可能会有一些优势,例如用于表示递归函数,但在打印和某些其他操作时会遇到困难,因此似乎可以建议不要使用循环列表结构。

用列表结构存储符号表达式的优点是:

  1. 程序必须处理的表达式的大小无法提前预测,甚至数量也是如此。因此很难安排固定大小的内存块来容纳它们。

  2. 不需要特定位置的内存时可以将其放回空闲内存列表中(见 §4.3 节)。即使只放回了一个机器字的内存也有意义,但如果表达式是线性存储的,则很难使用奇特大小的内存块。

  3. 同时出现在多个表达式中的子表达式只需在内存中表示一次。

4.2. 属性表(Property list)

在 LISP 程序系统中,我们在符号的属性表中放置的信息比前几节数学系统所需的要多。实际上,我们想把符号关联的所有信息全放属性表里。这些信息可能包括:打印名称,一个数字和字母组成的字符串,用于在机器外部表示符号;如果符号代表数字,还需要有一个数值;另一个 S-表达式,如果该符号以某种方式用作它的名称;或者例程的位置,如果符号表示的函数有机器语言子例程。所有这些都意味着,机器系统中基本元素比数学系统中持有更多信息。

现在我们只描述打印名称如何在属性表中表示,以便程序可以把打孔纸带、磁带或打印页面上的信息与机器内部的列表结构之间对应起来。符号 DIFFERENTIATE 的属性表有图 4 所示形式的一段数据。这里 pname 是一个符号,表示其后元素是符号的打印名称。在图 4 的第二行中,我们可以看到一个包含三个字的列表,每个字的地址部分指向包含 6 个六比特字符的字符串。最后一个字用了不可打印字符填充。(回想一下,IBM 704 字长 36 比特,可打印字符每个由 6 比特表示。)字符串数据的存在意味着属性表并不是 S-表达式,并且只有某些处理S-表达式的函数对属性表的操作才有意义。

4.3. 空闲内存列表(Free-Storage List)

在任意给定时刻,只有为列表结构保留的一部分内存实际上将用于存储 S-表达式。其余内存排列在 “空闲内存列表” 之中(在我们的系统中,程序开始时空闲内存列表里有约 15000 个机器字)。某个内存位置 FREE 包含此列表中第一字内存的位置。当需要一个机器字来形成一些额外的列表结构时,将获取空闲内存列表中的第一个字,并将 FREE 中的编号更改为空闲内存列表中第二个字的位置。用户无需编写将内存回收(return)到空闲内存列表的代码。

内存的回收是自动发生的,大致如下(有必要在本文章中简化该过程的描述):程序中有一组固定数量的根内存(base register),其中包含程序可访问的列表结构的位置。当然,由于列表结构分支,可以访问的列表结构会涉及任意大小的内存。程序可访问的内存之所以可访问,是因为它可以通过一系列 car 和 cdr 操作从根内存访问它。根内存的内容发生更改时,它以前指向的寄存器可能不再能被 car − cdr 链到达。可以把这样的内存视作被程序放弃的,因为任何合法的程序都无法再找到其内容;既然它的内容已经没用了,那就应该将其回收到空闲内存列表。这是通过以下方式实现的。

在程序用完可用存储空间之前,不会发生任何事情。当需要一个字的空闲内存,并且空闲内存列表中没有剩余的内存时,回收周期18将开始。

首先,程序查找可从根内存访问的所有内存位置,并把它们的值改为负数。更详细的步骤是,从每个根内存开始,更改从它开始的 car - cdr 链上每一个内存位置的符号位。如果此过程中遇到已经为负的内存位置,则认为该内存位置之前已经遇到过了。

在所有可访问内存的符号位都更改后,我们遍历为存储列表结构保留的内存区域,并将所有值为正的内存放回空闲内存列表中,然后把可访问内存的符号位改回来。

对程序员来说,因为这个过程完全自动,所以比手动跟踪和删除不需要的列表的系统更方便。如果可访问列表近乎耗尽可用内存,那么性能会变差。这是因为回收过程需要几秒钟才能完成,所以,除非想把大部分时间浪费在回收内存上,那么必须将至少数千个内存位置添加到空闲内存列表中。

4.4. 计算机上的基本 S-函数

我们现在将描述 atom、=、car、cdr 和 cons 的计算机表示。传递 S-表达式给函数时,我们会给表示后者的程序传递前者所在的字的地址。程序会以相同的形式返回 S-表达式。

  1. atom: 我们之前提到过,表示符号的字的地址字段是一个特殊的常量: atom 编译为检测该部分的所谓 “开放代码” 子例程。也就是说,M-表达式 atom[e] 编译成的代码将生成符号 T 或 F 作为测试的结果,除非它作为条件表达式中的条件出现,这时会编译为条件跳转指令,而不会生成符号 T 或 F。

  2. eq: eq[e; f] 编译为检测两机器字的地址是否在数值上相等的代码。这是正确的,因为每个符号只有一个属性表。与 atom 一样,编译的结果要么是条件跳转指令,要么是生成符号 T 或 F 的程序。

  3. car: car[​x] 获取内存位置 x 的地址字段的内容。这基本上是由单个指令 CLA 0, i 完成的,其中参数位于变址寄存器(index register)中,结果出现在累加器19的地址字段。(我们认为,函数获取参数和返回值的位置在函数的定义中规定,程序员或编译器有责任插入所需的数据转移指令,以获得计算的结果为后续计算备用。)(“car” 是 “寄存器地址字段的内容” (Contents of the Address part of the Register)的缩写,其中寄存器指内存位置。)

  4. cdr: cdr 的处理方式与 CAR 相同,不同之处在于返回值为 x 的减量字段(“cdr” 表示 “寄存器减量字段的内容” (Contents of the Decrement part of Register))。

  5. cons: cons[x; y] 返回内存位置,该处内存的地址字段和减量字段分别为 x 和 y。计算机中可能没有这样的内存,即使有,找到它也很耗时。所以实际上,我们从空闲内存列表中获取第一个字的可用内存,分别将 x 和 y 放在地址和减量字段,并让函数返回所取内存位置。(“cons” 是 “构造”(CONStruct)的缩写。)

cons 在空闲内存列表耗尽时会启动垃圾回收。现在使用的这版系统中,cons 是一个封闭代码的子例程。在编译版本中,cons 是开放代码的。

4.5. 用程序表示 S-函数

由 car、cdr 和 cons 构成的函数很容易编译,无论是手工转换还是用编译器。条件表达式更简单,只需要注意其中的 p 和 e 只有在需要时才被求值。递归函数的编译则是个麻烦。

一般来说(我们之后会讨论一个例外情况20),递归函数的例程将自身用作子例程。例如, subst[x; y; z] 的程序使用自身作为子例程,以求值在子表达式 car[z] 和 cdr[z] 中把 x 替换为 y 的结果。在计算 subst[x; y; cdr[z]] 时,必须将先前对 subst[x; y; car[z]] 的计算结果临时存储在某个内存位置。但是,求值 subst[x; y; cdr[z]] 时我们可能还需要一个这样的位置。这种冲突可以用栈21的 SAVE 和 UNSAVE 例程解决。递归函数在其例程开始时,启动 SAVE 例程,请求保存一组给定的连续内存块。被称为栈的内存块就是为此保留的。SAVE 例程可以访问一个 “索引” 变量,其表示栈中有多少个字的内存正在使用。它将要保存的内存位置的内容移动到栈中第一个未使用的字,递增索引,然后返回到启动 SAVE 例程之前的位置。之后程序就可以自由地把数据临时存放在这些位置上。在例程退出之前,它使用 UNSAVE,从栈中恢复临时内存的内容,并递减索引。用编程的话来讲就是 “递归子例程对临时内存是透明的”。

4.6. LISP 程序系统目前状态(1960 年二月)

我们已经把第 §3.6 节中描述的函数 apply 的变体转换为了 IBM 704 的程序 APPLY。我们知道该例程可以计算 S-函数的返回值,其中 S-函数用 S-表达式和参数表示,所以可以把它用作 LISP 编程语言的解释器,其中计算过程用 S-表达式表示。

现已嵌入 LISP 程序系统中的 APPLY 程序具有以下特点:

  1. 程序员可以通过 S-表达式定义任意数量的 S-函数。这些函数可以相互引用,也可以引用机器语言程序表示的某些 S-函数。
  2. 可以计算已定义函数的返回值。
  3. 可以读取和打印 S-表达式(直接或通过磁带)。
  4. 包括一些错误诊断和选择性跟踪功能。
  5. 程序员可能把指定的 S-函数编译成机器语言程序,放入核心内存。编译后性能差不多有解释时的 60 倍。编译速度还挺快的,所以没必要给编译后的程序打纸上22备用。
  6. “特色程序(program feature)” 允许程序包含类似 ALGOL 的赋值和 goto 语句。
  7. 系统中可以进行浮点运算,但效率低下。
  8. 正在编写程序员手册。LISP 程序系统适合那种数据可以自然地表示为符号表达式的计算,要求允许子表达式长得比较像外层表达式。IBM 709 系统的版本正在准备中。

5. 符号表达式函数的另一种形式

有许多方法可以定义符号表达式的函数,这些方法与我们采用的系统非常相似。它们中的每一个都涉及三个基本函数,条件表达式以及递归函数的定义,但与表达式和 S-表达式不同,函数的定义语法也不同。我们将描述其中一种称为线性 LISP (linear LISP)的变体。

L-表达式定义如下:

  1. 允许有限的字符列表。
  2. L-表达式是允许的字符组成的字符串。也包括空字符串,用 Λ 表示。

字符串有三个函数:

  1. first[​x] 是字符串 x 的第一个字符。我们不定义 first[Λ] 。

    举个例子, first[ABC] = A 。

  2. rest[​x] 是除了第一个字符以外剩下的所有字符组成的字符串。 rest[Λ] 同样是未定义的。

    例如:rest[ABC] = BC 。

  3. combine[x; y] 是将字符 x 拼接到字符串 y 之前构成的字符串。

    例如:combine[A; BC] = ABC 。

字符串上有三个谓词:

  1. char[​x],判断 x 是不是单个字符。
  2. null[​x],判断 x 是不是空字符串。
  3. x = y,只在 x 和 y 都是字符的情况下定义。

线性 LISP 的优点是没有特殊字符,包括常规 LISP 中的括号、点和逗号,因此我们可以使用任意线性编写的表达式进行计算。然而线性 LISP 也有缺点,子表达式的提取相当复杂,不像常规 LISP 那样非常基本。在线性 LISP 中编写与常规 LISP 基本函数相对应的函数并不难,因此,从数学上讲,常规 LISP 是线性 LISP 的一种。事实证明,操作变得复杂时,常规 LISP 就是编程最方便的线性 LISP。但是如果要把函数写到计算机上,则常规 LISP 本质上更快。

6. 流程图和递归

由于计算机程序的常用形式和递归函数都是通用的计算方式,所以它们之间一定有某种关系。递归函数转化为计算机程序是上文的主题。在本节中,我们将展示如何走另一条路,至少在原则上是这样。

计算机在计算过程中的任何时刻的状态可以用许多变量的值唯一确定。我们把这些变量组合成一个向量 ξ。考虑一个程序块,它刚好有一个入口和一个出口,那么它定义了一个函数 f(这个函数也反过来定义了该程序块),该函数将机器状态转换为另一个机器状态,也就是说,f 的形式为 ξ’ = f(ξ)。所以我们把 f 叫做该程序块关联的函数(associated function)。现在我们想组合多个这样的块,那么每个块结束之后,必然会进入另一个块,我们把这一步骤称作决策(decision)。决策的函数就记作 π 好了,它的参数自然就是 ξ,它返回几就表示该时刻的决策为第几条路径。多个块组合起来还是有刚好一个入口和一个出口。

以图 5 为例,让我们描述函数 r[ξ],它给出了向量 ξ 在整个块的入口和出口之间的变换。我们还需要定义函数 S[ξ] 和 t[ξ],它们分别是 ξ 在点 S 和 T 之间以及 S 和出口之间经历的变换。我们有

r[ξ] = [π₁[ξ] = 1 → S[f₁[ξ]];             
        π₁[ξ] = 2 → S[f₂[ξ]]]             

S[ξ] = [π₂[ξ] = 1 → r[ξ];                    
        π₂[ξ] = 2 → t[f₃[ξ]]]                         

t[ξ] = [π₃[ξ] = 1 → f₄[ξ];                
        π₃[ξ] = 2 → r[ξ];                    
        π₃[ξ] = 3 → t[f₃[ξ]]]   

给定一张刚好有一个入口和一个出口的流程图,不难定义入口到出口间状态向量变化的递归函数,其中需要用到计算块关联的函数,以及分支的决策函数。一般来说,我们按如下方式进行。

在图 6 中, β 是一个 n 向分支点,各个分支点记作 β₁, β₂, ⋯, βₙ ,决策函数是 π 。设 f₁, f₂, ⋯, fₙ 为各分支点的计算函数, ɸ₁, ɸ₂, ⋯, ɸₙ 为各分支点到出口的计算函数,那么 β 到出口之间机器状态的转换函数就是:

φ[ξ] = [π[ξ] = 1 → ɸ₁1[f₁[ξ]];        
        π[ξ] = 2 → ɸ₂[f₂[ξ]];        
                 …                           
        π[ξ] = n → ɸₙ[fₙ[ξ]]]       

7. 鸣谢

N. Rochester 注意到 λ 表示法在命名递归函数方面的不足,他发现了一种替代本文使用的涉及 label 的解决方案。可与其他函数组合的 cons 子例程形式由 IBM 公司的 C. Gerberick 和 H. L. Gelernter 与另一个程序系统一起发明。 LISP 程序系统由 R. Brayton、D. Edwards、P. Fox、L. Hodes、D. Luckham、 K. Maling、J. McCarthy、D. Park、S. Russell 等组成的小组开发。

该小组得到了 MIT 计算中心和 MIT 电子研究实验室的支持(该实验室部分得到美国陆军(通信兵)、美国空军(科学研究办公室、空中研究与发展司令部)和美国海军(海军研究办公室)的支持)。作者还希望感谢 Alfred P. Sloan 的个人财务支持。

参考文献

  1. J. McCARTHY, 《Programs with common sense》(标题大意:常识程序, 链接), 1958 年 11 月 24 日至 27 日,于英国特丁顿国家物理实验室,思维过程机械化研讨会(Symposium on the Mechanization of Thought Processes)上发表的论文。(发表在 H. M. 文具办公室的研讨会论文集上)。
  2. A. NEWELL 和 J. C. SHAW, 《Programming the logic theory machine》(标题大意:逻辑理论机器的编程,链接), Proc. 西部联合计算机研讨会, 1957 年 2 月。
  3. A. CHURCH, 《The Calculi of Lambda-Conversion》(标题大意:λ 演算)(普林斯顿大学出版社,新泽西州普林斯顿,1941 年)
  4. FORTRAN Programmer’s Reference Manual(标题大意:FORTRAN 程序员参考手册), IBM 公司,纽约,1956 年 10 月 15 日。
  5. A. J. PERLIS 和 K. SAMELS0N, 《International algebraic language, Preliminary Report》(标题大意:国际代数语言,初步报告,链接), CACM, 1958 年 12 月。
8 个赞

感谢啊!我还收藏了一个十分模糊的pdf版,且一直没看 :grin:

stanford.edu/jmc 有高清的

1 个赞

我看到这些数学符号我就头晕 :rofl: