先見之明但並非完美:回顧 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 可以迭代幾次,那麼這篇文章將會更加完美!

© .