先见之明但并非完美:回顾 1966 年《大众科学》关于系统分析的文章

加入我们的科学爱好者社区!

本文发表于《大众科学》的前博客网络,反映了作者的观点,不一定代表《大众科学》的观点


主编的话

《大众科学》正在庆祝其 166 周年。作为美国出版时间最长的连续出版杂志,它触动了许多读者的生活和职业道路,包括为我们撰写文章并报道其工作的科学家,这可能并不奇怪。因此,正如经常发生的那样,当我在担任 谷歌科学博览会 的评委时遇到谷歌研究主管彼得·诺维格时,我们开始聊起了《大众科学》。他提到该杂志对他个人影响深远。虽然对他最具启发性的文章在许多方面被证明是正确的,但最终在其他方面却是错误的。我说了类似这样的话:“这真的很有趣。我想知道那些是什么——我敢打赌其他人也会想知道。你愿意为我们写一篇关于这个的文章吗?”

我很高兴分享诺维格精彩而详细的回复。如果编程细节激起了您对他的更多想法的渴望,您甚至可以考虑注册他秋季在斯坦福大学的人工智能入门课程,他和同事塞巴斯蒂安·特伦将在网上免费提供该课程。


关于支持科学新闻

如果您喜欢这篇文章,请考虑通过以下方式支持我们屡获殊荣的新闻报道 订阅。通过购买订阅,您正在帮助确保有关塑造当今世界的发现和想法的具有影响力的故事的未来。


我们会不时提供有关《大众科学》的其他见解——既有来自与杂志读者分享研究世界的科学家,也有来自我们的员工和撰稿人,他们可以为您提供有关我们正在进行的新项目的内部消息。一如既往,我们很乐意收到您的评论和反馈。如果您受到《大众科学》的启发,请与我们分享您的故事:submit@sciam.com

—玛丽埃特·迪克里斯蒂娜

系统分析和编程:阁楼里的想法

在 20 世纪 70 年代初,我十几岁的时候,我喜欢去阁楼里翻阅旧的《大众科学》杂志。(那时我们还没有维基百科。)我最渴望的是九月刊,这些期刊专门讨论一个主题领域。我记得读过关于 生命(1961)宇宙(1956)数学(1964) 的期刊。

但对我影响最大的是 1966 年 9 月关于 信息 的期刊(我大约 40 年前读过)。该期刊汇集了一批杰出的作者,他们现在被公认为计算机科学的先驱领导者:埃文斯和萨瑟兰解释了计算机硬件;法诺和科尔巴托讨论了操作系统;托尼·奥廷格描述了他的自然语言解析器;以及我自己的子领域(人工智能)的两位巨头麦卡锡和明斯基讨论了信息论和人工智能。如果我能够理解这期期刊中的所有内容,我本可以在计算机科学方面的教育时间缩短十年。

在这篇文章中,我将重点关注该期刊中的一篇文章:克里斯托弗·斯特雷奇 撰写的关于 “系统分析和编程” 的文章。当时,我只看过一些 BASIC 代码片段——不超过几行。斯特雷奇的这篇短文是我第一次接触高级编程语言,也是我读过的第一个非平凡程序:一个跳棋程序。当我最近重新发现这篇文章时,我惊讶地发现了两件事:(1) 编程语言和编程风格非常现代,(2) 设计和实现之间,或者斯特雷奇所说的系统分析和编程之间存在严重的不匹配。让我们看看斯特雷奇在哪些方面做得出奇地好,他犯了哪些错误,以及他根本没有包含哪些内容。

优点、缺点和缺失

虽然斯特雷奇在 40 多年前写了他的文章,但它在许多方面都出奇地有先见之明和前瞻性。我们将从一些大部分好的部分开始

优点: 在第一句话中,斯特雷奇就认识到编程“几乎总是比你预期的要花费更长的时间”。他接着描述了一种 21 世纪前沿的教育理念

“我认为,问题在于许多教育过程都非常重视第一次就得到正确答案。如果你在考试中给出了错误的答案,你就会失去分数,事情就结束了。如果你在编写程序时犯了错误——或者实际上,在教室外的许多其他情况下——这绝不是一场灾难;你确实需要找到你的错误并纠正它;也许如果更多的学术教学也采用这种态度会更好。”

如果 45 年前每个人都采用了斯特雷奇的方法,今天的世界会好多少?

优点: 当时大多数程序员都在使用汇编程序,而第一批计算机语言(LISP、FORTRAN 和 COBOL)只有大约八年的历史,斯特雷奇选择使用一种非常高级的编程语言 CPL。CPL 令人惊讶地焕然一新,让人联想到当今流行的语言,如 Python 或 Ruby。

缺点: CPL 非常新,没有编译器,也没有完整的正式描述。来自 1963 年1968 年 的期刊文章以及来自 2000 年 的死后出版的笔记部分描述了该语言的版本,这些版本与文章中呈现的版本略有不同。我尽力接近 CPL 语言的真实面貌,并为其编写了 一个转换器 (见下文),我们可以用它来测试和调试斯特雷奇的程序。

优点: 斯特雷奇承认测试的重要性,他说他的程序“还没有在机器上运行,因此,根据下面表达的观点,可能仍然包含一些错误。”

优点: 这是一项精湛的编程技艺;特别是考虑到代码都是手工编写的,没有编译器、调试器或其他我们习惯使用的工具。如果你是一名程序员,你上次在没有这些工具的情况下编写 70 行代码,并且几乎全部正确是什么时候?斯特雷奇在这方面功不可没。但是…

缺点: 该程序确实至少包含一个错误(我们将在下面看到)。

优点: 关于“系统分析和编程之间的区别不是很有用”的建议。当时普遍的文化强调瀑布模型,在这种模型中,分析首先进行,只有在分析完成后,过程才会流入编程。今天,大多数人对螺旋模型更适应,在这种模型中,有更快的迭代:先进行少量分析,然后进行一些编程,然后进行更详细的分析,等等,或者使用敏捷模型,在这种模型中,迭代速度非常快。通过模糊界限,斯特雷奇似乎更倾向于敏捷/螺旋方面。

现在我们将更多地关注一些负面因素,这些因素主要与文本中对程序的描述和代码中的实现不匹配有关

缺点: 不幸的是,斯特雷奇似乎没有遵循他自己关于交替进行分析和编程的建议。我假设他是分析师和程序员,因为他是唯一的作者。分析非常好。编程也是如此。但似乎斯特雷奇这位分析师从未与斯特雷奇这位程序员进行过太多交流,因为实现偏离分析很远。例如,分析描述了一个返回对棋盘位置 P 的可取性的数值估计的函数“PositionValue(P)”。但是在检查代码时,会发现函数“PositionValue(P,d)”。斯特雷奇这位程序员添加了第二个参数,却忘记告诉斯特雷奇这位分析师。因此,我们看到了一个古老问题的早期例子:文档与代码不匹配。发生了什么?斯特雷奇这位分析师暂时忘记了他的策略是向前看几步,然后评估位置。为了确定要向前看多远,您需要知道您已经看了多深;这由参数 d 表示。斯特雷奇这位程序员可以通过将深度记录为位置的一部分来保持函数签名 PositionValue(P),但他选择将其作为单独的参数。无论哪种选择都可以,但在做出选择后,他应该返回并更新设计文档。

优点: 重点关注如何“表示游戏的相关特征,以及我们希望能够对它们执行哪些类型的操作?”斯特雷奇这位分析师正确地指出,编程的全部内容是识别对象类型以及它们可以参与的操作。他是面向对象的,但不是对象痴迷的——对于 1966 年来说,这是一个非常开明的观点。在这种特殊情况下,他指出跳棋涉及位置、移动和与之相关的值。然后,斯特雷奇这位程序员实现了将棋盘位置表示为 四元组,包含以下元素:轮到哪个玩家、该玩家棋子的位置、对手棋子的位置以及王的位置。

差评: 不幸的是,程序员斯特雷奇从未定义移动的表示方式。设计文档中描述了“MakeMove(M,P)”函数,用于从位置 P 执行移动 M,而实际实现中定义的是“MakeMove(P,C,s,F)”,其中 F 是正在移动的棋子,s 是移动方向,C 表示这是吃子还是非吃子移动。你可能会认为可以将这三个参数打包成一个元组 M = (C,s,F),然后就可以使用 MakeMove(M,P) 了。不幸的是,(C,s,F) 记号无法表示双跳(或三跳等)。实际上,代码中没有任何地方显式表示多重跳跃的概念(而是将单独的跳跃逐个进行,生成一系列中间位置)。同样,分析中谈到了函数“LegalMovesFrom(P)”,但没有实现这样的函数。相反,我们得到了“LegalPositionsFrom(P)”,它枚举了执行所有合法移动(包括多重跳跃)后产生的位置,但没有表示移动本身。拥有 LegalPositionsFrom 而没有 LegalMovesFrom 不一定是不好的,但是文档与代码完全不同步是很糟糕的。

好评: 斯特雷奇很好地展示了如何使用函数式编程风格。这在当时是非常了不起的,因为当时最流行的高级语言是 FORTRAN,它不支持递归,并且大部分控制流都是通过 GOTO 语句处理的。

差评: 不幸的是,斯特雷奇没有使用任何高阶函数来构建程序,仅仅依赖于递归。这相当于只依赖 GOTO 语句,而不是循环,这意味着他最终使用了辅助函数,这些函数的名称反映了它们在控制流中的作用(例如 PCP 或 PartialCapturePositionList),而不是独立的目的(例如 MakeMove)。读者必须通过逆向工程递归调用来揭示控制流。

例如,以下是斯特雷奇如何实现 ChosenPosition 的,该函数从当前位置 P 中选择最佳的移动位置

ChosenPosition(P) = value of
§ let L = LegalPositionsFrom(P)
if Null(L) then Resign
let (p,v) = BestPosition(NIL, - 8,L,0)
result is p §

BestPosition(P,V,L,d) = Null(L) ? (P,V), value of
§ let (p, l) = Next(L)
let v = - PositionValue(p,d + 1)
result is (v > V) ? BestPosition(p,v,l,d),
BestPosition(P,V,l,d) §

读者需要认识到 BestPosition 的前两个参数保存了到目前为止找到的最佳位置和值,而第三个参数 L 是剩余待考虑位置的列表。在研究该函数一会儿后,人们会意识到它正在根据函数 PositionValue 计算最大值位置(PositionValue 给出了待移动玩家的值,因此您需要对对手的位置值取反)。要理解这一点需要一些脑力劳动。

我更喜欢一种方法,这种方法可以立即清楚地表明 ChosenPosition 正在根据 PositionValue 计算最大值,因此我会像下面这样编码:

ChosenPosition(P) = Max(LegalPositions(P), PositionValue)

这里我假设位置的表示略有不同,其中深度被编码为位置的一部分(您可以通过注意深度是奇数还是偶数来处理对手分数的取反)。函数 Max 接受两个参数:一个项目列表和一个计算项目值的函数。Max 返回根据该函数具有最高值的项目。我认为这个版本的 ChosenPosition 更容易理解。这种形式的 max 函数是 Python 语言内置的,但可以很容易地在 CPL 中定义如下:

Max(Items, ValueFunction) = value of
§ (Best, BestVal) = (NIL, -8)
while Items do §
(Item, Val) = (Head(Items), ValueFunction(Head(Items)))
if Val > BestVal then (Best, BestVal) := (Item, Val)
Items := Rest(Items) §
result is Best §

我的实现与斯特雷奇的不同之处在于两点。首先,关注点分离:我将寻找最大值的概念从寻找具有最大值的合法位置的具体细节中抽象出来,而斯特雷奇则将这两个概念混淆在一起。其次,重用:一旦我定义了 Max,我就可以一遍又一遍地使用它。

现在让我们稍微改变一下思路,从现代的观点来看,考虑一下斯特雷奇遗漏了哪些在当今系统分析讨论中会涵盖的内容。

缺少测试过程。斯特雷奇煞费苦心地解释说会出现错误,但他没有描述解决这些错误的过程。他将创建软件的过程分为系统分析和编程;更现代的观点是,测试和维护也是该过程的重要组成部分,它们有自己的方法和产品。他至少应该包括一个测试套件。斯特雷奇忽略了计算复杂性主题。他知道跳棋是一个过于复杂的游戏,无法完全解决(跳棋大约有 10^20 个位置需要评估)。但他没有描述他的算法的运行时,也没有描述它如何依赖于搜索树的最大深度,也没有描述可以使用 alpha-beta 算法 而不是 minimax 来加快进程。假设你想为人们运行一个玩跳棋的网站。什么架构可以让你每秒扩展到数百或数千次并发移动?你需要多少计算能力?你如何保护系统免受恶意入侵者的攻击?这些都是现代世界常见的担忧,但在斯特雷奇的时代却不是。斯特雷奇根本没有讨论显示跳棋盘的问题,也没有讨论与玩家的交互。但是,设计良好的用户界面现在被认为是任何软件项目的重要组成部分。关于大规模编程的讨论不多。 斯特雷奇说,像 CPL 这样的高级语言使个人可以编写 50,000 条机器指令的程序,而团队可以达到 50 万条。但是今天,我们有许多团队正在创建包含数千万条指令的大型系统,以及一些包含数亿条指令的系统。尽管您可能会听到很多关于当今系统质量的抱怨,但我们一定做对了某些事情,才能在 40 年内将我们处理复杂性的能力提高 100 倍。

现在我们将直接深入了解斯特雷奇的跳棋程序本身。

跳棋程序

这是斯特雷奇在第 117 页提供的代码。我尽力逐字复制了原文(包括复制了标点符号周围不一致的空格用法)

ChosenPosition(P) = value of
§ let L = LegalPositionsFrom(P)
if Null(L) then Resign
let (p,v) = BestPosition(NIL, - 8,L,0)
result is p §
BestPosition(P,V,L,d) = Null(L) ? (P,V), value of
§ let (p, l) = Next(L)
let v = - PositionValue(p,d + 1)
result is (v > V) ? BestPosition(p,v,l,d),
BestPosition(P,V,l,d) §

PositionValue(P,d) = Terminal(P,d) ? TerminalValue(P), value of
§ let L = LegalPositionsFrom(P)
let (p,v) = BestPosition(NIL,- 8,L,d)
result is v §

LegalPositionsFrom(P) = value of
§ let L = RemainingPositionList(P,Capture,5)
result is Null(L)?RemainingPositionList(P,NonCapture,5),L §

RemainingPositionList(P,C,s) =
PartialPositionList(P,C,s,FindMoveList(P,C,s))

PartialPositionList(P,C,s,L) =
Null(L)?( (s = -5)?NIL,
RemainingPositionList(P,C,NextMoveDirection(s) ),
value of
§ let F = SingleDigitFrom(L)
let Ip = MakeMove(P,C,s,F)
let l = (C = Capture)?CaptureTree(Ip),
FinalPosition(Ip)
result is Join(l,PartialPositionList(P,C,s, L - F))§

NextMoveDirection(s) = (s = 5) ? 4, ( (s = 4) ? - 4, -5)

FindMoveList(P,C,s) = value of
§ let (X,Y,K,s) = P
let Empty = ~ X ? ~ Y ? Board
let ? = (C = Capture) ? (Shift(Empty,ss) ? Y), Empty
let F = Shift(?, ss) ? X
result is (s > 0) ? F, F ? K §

MakeMove(P,C,s,F) = value of
§ let (X,Y,K,s) = P
let ? = (C = Capture) ? Shift(F, - ss), NIL
let ? = (C = Capture) ? Shift(?, - ss),
Shift(F, - ss)
let Xk = Null(F ? K) ? (? ? LastRows), (? - F)
result is ((X - F + ?), (Y - ?), (K - ? ? K + Xk),s,?)§

FinalPosition(Ip) = value of
§ let (X,Y,K,s,F) = Ip
result is (Y,X,K, - s)§

CaptureTree(Ip) = value of
§ let L = PartialCapturePositionList(Ip)
result is Null(L) ? (FinalPosition(Ip) ),
CombineCaptureTrees(L) §

PartialCapturePositionList(Ip) = value of
§ let (X,Y,K,s,F) = Ip
let P = (X,Y,K,s)
result is MinList(PCP(P,F,5),PCP(P,F,4),
PCP(P,F ? K, - 4), PCP(P,F ? K, - 5) )§

PCP(P,F,s) = value of
§ let (X,Y,K,s) = P
let ? = Shift(F, - ss) ? Y
let Empty = ~ X ? ~ Y ? Board
let ? = Shift(?, - ss) ? Empty
let Xk = Null(F ? K) ? (? ? LastRows), (? - F)
result is Null(?) ? NIL,
((X - F + ?),(Y - ?),(K - ? ? K + Xk),s,?)§

CombineCaptureTrees(L) = Null(L) ? NIL, value of
§ let (Ip,l) = Next(L)
result is Join(CaptureTree(Ip),CombineCaptureTrees(l) )§

位置的表示

斯特雷奇使用了一种巧妙的棋盘位置表示法,这种表示法可以追溯到亚瑟·塞缪尔的 1959 年程序文章。他将位置编号如下:

请注意,此表示不是密集的;某些编号的方格(13、22、31)不在棋盘上。这种方法的优点是,每个方格与其对角线邻居相距 4 或 5 个位置。如果他使用标准的跳棋表示法,将方格编号为 1 到 32,那么某些方格与其邻居相距 4 或 5 个位置,而某些方格相距 3 或 4 个位置。

给定此编号,棋盘位置是一个四元组 (X, Y, K, s),其中 s 是要移动的玩家,而 X、Y 和 K 是位模式,在有棋子的方格编号中为 1。X 位模式表示玩家的棋子。Y 表示对手的棋子;而 K 表示国王(双方的)。

CPL 的翻译器

斯特雷奇在 1966 年撰写文章时没有 CPL 编译器。第一个 CPL 编译器大约在 1970 年出现,到 1980 年代就消失了。因此,我必须创建自己的编译器才能运行和调试跳棋程序。我必须做出三个选择:(1) 哪些 CPL 语法是合法的,它们的含义是什么? (2) 我应该使用什么解析器生成系统来创建 CPL 翻译器? (3) 我应该翻译成哪种语言?完整的编译器(即机器语言的翻译器)是一个相当复杂的项目,但是翻译成与 CPL 非常相似的高级语言要容易得多。

(1)CPL 的大部分内容都很直观,但我在阅读了关于 CPL 的 1963 年1968 年 的文章后得到了一些帮助。我了解到“一个块由一个或多个定义,后跟一个或多个命令组成,整个块都包含在节括号内。”然而,跳棋程序并不遵循块的这种定义,因为在第一个函数定义 ChosenPosition 中,我们有一个块,其中包含一个定义 (let L),后跟一个命令 (if Null(L)),后跟另一个定义。根据 1963 年的论文,我们应该为第二个定义设置第二个块。因此,要么 Strachey 在跳棋程序中犯了一个错误,要么他使用的是不同版本的 CPL。我决定接受在任何地方进行定义,而不仅仅是在块的开头。这意味着“value of”和“result is”是空操作;函数的结果始终是最后一个表达式。我不确定这是否是 CPL 的本意,但这种解释适用于跳棋程序中的所有代码。

我从 1963 年的论文中学到的另一件事是,CPL 有两种变量名:小名称,即单个小写字母(或者根据跳棋程序推测是希腊字母),和大名称,以大写字母开头,后跟任何字母。小名称不需要用空格分隔,并且似乎允许隐式乘法,因此“ss”等同于“s * s”。

(2)对于解析器生成器,我选择了 YAPPS2(Yet Another Python Parsing System),主要是因为该系统的作者 Amit Patel 是一个坐在附近的的朋友,所以如果我遇到麻烦,我知道我可以走到大厅寻求帮助。(事实证明他的系统非常易于使用,我不需要任何帮助。干得好,Amit!)

(3)CPL 是一种灵活的、主要为函数式的语言,因此 Lisp 似乎是一个很好的目标语言。但是 CPL 使用中缀表示法;我必须正确地获得所有运算符的优先级,才能生成带有正确位置的括号的 Lisp 代码。所以我决定生成 Python 代码,并忽略运算符优先级的问题:我将 CPL 代码“(K – ? ? K + Xk)”翻译成 Python 代码“(K – psi & K + Xk)”,并希望运算符的优先级能够正常工作。但是,使用 Python 存在一个问题:它严格区分了表达式和语句(语句不能出现在表达式内部),而 CPL 没有这种严格的区别。因此,我生成仅使用表达式的 Python 子集中的代码。例如,给定 CPL 函数

LegalPositionsFrom(P) = value of
§ let L = RemainingPositionList(P,Capture,5)
result is Null(L)?RemainingPositionList(P,NonCapture,5),L §

最直接的 Python 翻译是

def LegalPositionsFrom(P)
L = RemainingPositionList(P, Capture, 5)
return (RemainingPositionList(P, NonCapture, 5) if Null(L) else L)

对于此函数,这将正常工作。但通常,CPL 块可以是嵌入在另一个表达式中的表达式。Python 语句(例如“L = ...”和“return ...”)不能出现在表达式内部。所以我们不会使用它们;相反,我们将 CPL 块翻译成 Python lambda,然后使用语法“(lambda …)().”立即调用它。此调用不传递任何参数给 lambda(因为它只是一个代码块);但是,我们可以在块内通过定义 lambda 表达式的可选参数及其默认值来引入新的局部变量。所以我们有

def LegalPositionsFrom(P)
return (lambda L=RemainingPositionList(P, Capture, 5)
(RemainingPositionList(P, NonCapture, 5) if Null(L) else L))()

请注意,“一切都是表达式”规则有一个例外:每个函数定义都是“def f(x): return expr.”形式的语句。

1963 年的文章还明确指出,CPL 具有与整数数据类型不同的逻辑数据类型。在 C 和 Python 等语言中,位模式 0b1111 与整数 15 相同;它们都属于 int 类型。但是在 CPL 中,两者是不同的。我使用 Python 中的 Logical 类实现了 CPL 逻辑数据类型。我需要它的原因是表达式 int(0) - int(1) 应该是 -1(因为这里的减号是普通的整数减法),但是 Logical(0) - Logical(1) 应该是 Logical(0)(因为这里的减号是逻辑位集差运算符)。Logical 类具有通常的运算符集(and、or、not 等),并且具有一个构造函数,该构造函数将位模式或设置的位列表作为输入。因此,我们可以使用 Logical(0b1111) 或 Logical([0,1,2,3]) 来表示最右边四个位为 true 的逻辑位集。

做出这些选择后,我准备好用 YAPPS 形式编写 CPL 的语法,并将其翻译成 Python。原始代码使用希腊字母和数学符号(§、V、?、?);我选择使用 HTML 实体来表示这些(而不是 Unicode)。您可以在文件 cpl.g 中看到语法。

跳棋程序的其余部分

《大众科学》限制了 Strachey 的页数,因此没有显示整个程序。他省略了 CPL 的一些基本原语(例如 Shift,它对位字符串进行逻辑移位)以及跳棋程序的某些似乎并不重要的部分(例如打印棋盘)。您可以在文件 checkers.py 中或在 带注释的网页 中看到这些缺失的部分。

调试跳棋程序

以下是我在实现 CPL 翻译器和跳棋程序时发现的错误/误解

CPL 语言:如上所述,我必须修改 CPL 块的定义,以允许混合定义和语句,而不仅仅是先定义后跟语句。文章中的拼写错误:文章中的代码在 PartialPositionList 中遗漏了一个右括号(在第三行末尾);我把它加上了。NIL:文档说 NIL 是“空列表”。实际上 NIL 有三种用法:(1)作为值无关紧要的占位符,(2)作为空列表,以及(3)作为所有零的位字符串。对 (2) 和 (3) 的混淆让我感到困惑。我想我可以修改我的基本例程的实现以适应这种混淆,但我将程序中 NIL 的一种用法更改为 Zero,即全零位字符串。Join 和 MinList 的定义。Strachey 的文档说 Join(L1,L2) 产生“由 L1 和 L2 的成员形成的单个列表。”这就像 Lisp 函数 append。但是在程序中,它与第一个参数一起使用,该参数是一个单元素,而不是一个列表,就像 Lisp 函数 cons 一样。我更改了 Join 的定义以允许这样做。类似地,MinList(L1,L2…) 被记录为产生“由多个列表的成员形成的单个列表,并且还会省略空列表和重复项。”但实际上它没有传递列表,而是传递单个位字符串(或 NIL)。同样,我更改了我的函数以反映其在程序中的实际用法,而不是文档中的用法。处理国王。我印象深刻的是,通过上述小的调整,该程序可以完美地处理普通棋子的移动和跳跃。但是它在处理国王方面表现不佳。我不明白表达式 (K – ? ? K + Xk) 应该如何在移动后给出棋盘上所有国王的位置。也许我仍然对位字符串运算符的含义有所误解,也许该表达式实际上是正确的,但是在我的理解下,一个清晰且正确的表达式是 (K – ? – F + Xk)。此表达式表示从 K 开始,即所有原始国王位置的位字符串;然后删除 ?,即被捕获的棋子的位置(如果有);删除 F,即移动的起始位置;并添加 Xk,即移动的结束位置,如果(a)结束位置在最后一行,或者(b)移动的棋子在移动开始时是国王;否则 Xk 为 Zero 位字符串。我修改了 MakeMove 和 PCP 来做到这一点。通过这个最终的更改,该程序通过了我的完整 测试套件

总的来说,对于 1966 年来说,这是一个非常出色的软件。除了微不足道的遗漏括号(我认为这可能是《大众科学》排字员的错误,而不是 Strachey 的错误)之外,代码中只有一个真正的错误,即对国王的处理。(而且 Strachey 的表达式实际上可能是正确的,但我对他意思的理解有误。)

然而,软件和文档之间的对应关系很草率。函数上的参数数量记录错误;并非所有记录的函数都已实现;列表和位字符串之间的概念混淆。如果分析师 Strachey 和程序员 Strachey 可以迭代几次,那么这篇文章将会更加完美!

© . All rights reserved.