簡體中文書名: 计算机程序的构造和解释
20131024 淘寶訂單 20131104 到手, 12 天才收到, 有點久。
費用 42 * 5.1 = 214.2 + 145 = 359.2 委託代買購得。
這本書有電子版本, 當然是英文的, 要不然我就不用花錢買中文的版本。
Peter Norvig, paul graham 給了 5 星評價。
簡體中文版本看來薄薄的, 可別以為是本小書, 整整有 472 頁, 所以紙質真的不怎麼樣 (薄的厲害, 我覺得用印表機得到的品質都比這本書好), 在上頭寫注記得小心點, 一不小心可能就劃破紙張。在閱讀上也有點困擾, 整本書歪斜的很厲害, 不好拿著看。
簡體中文版本出版日期: 2004 年 2 月 1 日, 我這本是 201306 第八刷, 原文書 1984 年出版,是美国麻省理工学院 (MIT) 多年使用的一本教材, 1996 年修订为第二版。
後來 MIT 开始用 python 教授此课,代替了原本的 Scheme: Why MIT switched from Scheme to Python
淘寶有些書是影印的版本, 還好這本不是, 我本來還擔心這本已經出版了這麼久的一段時間, 會買不到正版的, 不過看來這本書還有陸續再出版。
我的進度很緩慢, 不過我很有耐心和毅力, 忍住開發 os, qt for android, uefi 程式的相關誘惑, 專心看這本書, 慢慢地, 慢慢地, 我越來越可以看下去了, 速度也提升了一點點。她有什麼魔力讓我願意這樣做呢? ref 2 提到: 您没法从中学到如何开发一个网站,开发一个记事本,如何绘图,它完全是在锻炼程序员的基本能力,而不是“技术”。一言以蔽之, 就是在教怎麼寫程式。很推薦大一同學和程式初學者讀這本, 這本書並不是進階書而是入門書哦! 但這不表示它很好讀, 請有心理準備。和一般的程式語言書籍不同, 裡頭的數學味道很重, 幾乎都是數學題目, 牛頓法、最大公因數、微分, 有理數/複數的運算, 真是不習慣。更不習慣的是他所選用的語言是 lisp 系的 scheme, prefix syntax 讓人迷惑, 這是吸引我的最主要部份, 據說 2008 年已經改用 python (好像是 2009), 若是 python, 我也許就沒興趣了吧! 看著一堆小括號充滿螢幕, 真是有趣。
4.1.1 ~ 4.1.4 會寫一個 scheme interpreter, 我覺得還是照課本使用 scheme 來讀 sicp, 比較不會有和課本出入的地方。
這本教科書我讀來實在辛苦, 覺得很難, 所以讀得很慢, 不知道是不是因為簡體中文的翻譯, 我覺得又讓她更難讀了。译者裘宗燕老師學問應該很好, 不過翻譯可能不是他的強項 (他翻譯過很多不錯的原文書籍)。
In a production-quality Lisp system,
書中翻成 "在產品質量的 Lisp 系統裡", 說實在, 我得看原文才懂這句話。這英文不算難懂, 但要翻譯成通順的句子那就不容易了。
It calls apply-primitive-procedure to apply primitives;
書上翻譯: 它直接調用 apply-primitive-procedure 去應用基本過程
我把它改為: 它直接呼叫 apply-primitive-procedure 去執行 primitives procedure。
To define a variable, we search the first frame for a binding for the variable
我們需要在第一個框架查找該變量的約束
descent 改: 我們需要在第一個框架查找該變數的定義
還真的不好翻譯, 只看中文會看不懂, 查詢完原文後就好多了。
This computation will run much more slowly than a gcd procedure written in
Scheme, because we will simulate low-level machine instructions, such as
assign, by much more complex operations.
書上翻譯為:
因為我們是在模擬低級的機器指令, 例如 assign, 而使用的卻是比他高級得多的操作。
這句話也是怪怪的。
書中的句子有很多像是這樣的翻譯, 讀來不能算是輕鬆, 我偶爾得對照一下原文詞句。
就算翻譯的不算好, 但中文版本還是幫了我不少忙, 這本並沒有爛到完全看不懂, 大多時候還是可以理解翻譯後的意思, 沒有中文翻譯, 我無法那麼快瀏覽過一遍, 有些翻不好的地方如果不是關鍵處, 倒也無傷大雅。加上對照原文, 對於英文不好的我來說, 甚有幫助, 這是翻譯的價值。與其要求每個人把英文、日文、德文、法文學好, 才能吸收其中的專業知識, 倒不如用國家力量培養優秀的翻譯人員。
前 言 第1章 构造过程抽象 1.1 程序设计的基本元素 1.2 过程与它们所产生的计算 1.3 用高阶函数做抽象 第2章 构造数据现象 2.1 数据抽象导引 2.2 层次性数据和闭包性质 2.3 符号数据 2.4 抽象数据的多重表示 2.5 带有通用型操作的系统 第3章 模块化、对象和状态 3.1 赋值和局部状态 3.2 求值的环境模型 | 3.3 用变动数据做模拟 3.4 并发:时间是一个本质问题 3.5 流 第4章 元语言抽象 4.1 元循环求值器 4.2 Scheme的变形——惰性求值 4.3 Scheme的变形——非确定性计算 4.4 逻辑程序设计 第5章 寄存器机器里的计算 5.1 寄存器机器的设计 5.2 一个寄存器机器模拟器 5.3 存储分配和废料收集 5.4 显式控制的求值器 5.5 编译 |
這是本怎麼樣的書呢? 主要在教如何寫程式, 以及程式是怎麼樣的一個東西。前兩章對一個有涉獵程式的人來說, 大概都能明白, 也就是一般 c++ 程式入門書籍上都會談到的概念。
資料抽象 - 將資料結構和操作的函數分開, 形成一個黑箱, 資料結構的改變不會影響這些操作函數, 將修改程度降到最低。這不是一般的 c++ 程式書籍都會提到的資料封裝嗎。
用不同的資料結構來表示同一個抽象資料, 書中提到複數的例子, 可以用直角座標或是極座標 (很久沒聽到這名詞了吧?) 來表示複數, 但也必須要在這兩種資料結構做轉換。都是表示複數, 沒道理處理直角座標的程式碼無法在極座標上執行, 該怎麼做呢? 最直觀就是提供轉換的函式, 這不也是在程式設計中會看到的技巧。c++ 可以實作 cast operator 和這個觀念很類似。
再來型別轉換一多的話, 程式碼會太複雜, 能辨識出型別的操作函數不是很棒, 怎麼做? 加個 tag 判斷即可。這不就是 if/switch/case 的應用嗎?
if circle do_circle else do_others
而 c++ 為了消除太多 if/switch/case 導入了 virtual function, 這邊也用了類似表格法來處理這樣的問題, 類似 c 的 function pointer。這些技巧都不會因為用什麼語言而有所不同, 是基本中的基本。
但從第三章開始, 就不是一般程式設計書籍提到的東西了, 你曾經想過變數與變數值的對應是怎麼實作的嗎? 在 script language 裡頭, 執行一個 expression 時會發生怎麼樣的行為呢? 函式的參數如何替換成真正的值? 3.2 有個概括的說明。
而第四章第五章的 metacircular evaluator, register base machine 更不是容易見到的主題。我實在很懷疑這樣的書籍竟然只是定位在入門等級。
從第一章看起 ...
1.1.1 談 expression
1.1.4 利用 define 來建立 procedure (如同 c function)
1.1.5 談到了 applicative order, normal order, p13 練習 1.5 有個有趣的題目:
當執行 (p) 會怎麼樣, (p) 會先去找出 p 是什麼, 結果又找到 (p), 然後又去找 p 是什麼 ... 永遠都找不到, 這就是 applicative order。
而 normal order, 就很順利執行完畢, 因為 normal order 不需要把 p 計算出來, 有需要的時候在計算即可。
在 mit-scheme 可以使用 delay 這個函數來執行 normal order, (delay (p)) 就不卡住了。
ref:
- http://www.gnu.org/software/mit-scheme/documentation/mit-scheme-ref/Promises.html
- http://practical-scheme.net/gauche/man/gauche-refe_59.html#Delay-force-and-lazy
1.1.6 conditional expressions and predicate, 就是 if/else 用法。
這樣讀過來之後, 就可以開始用 scheme 寫程式了。不過和 c 語言不同, 沒有 for/while loop 這種東西, 那替代品是什麼呢? 是讓人傷透腦筋的 recursive。
1.1.7 從牛頓法開始求根號 X, 正常人應該都忘記什麼是牛頓法, 只記得牛頓是誰吧?
in mit-scheme
2 error> (load "square_root.scm")
;Loading "square_root.scm"... done
;Value: 1.4142156862745097
書上有寫的東西就不說明了, 這種語法, 還真是不習慣。
1.2.1 linear recursion and iteration, 介紹 recursive 和 iteration, 書中用了兩個 factorial 來解釋。
iteration 的版本看起來好像是 recursive, 但這和 c 語言的 while, for loop 是一樣的概念。
1.2.3 介紹計算所需的時間, 計算所需要的記憶體空間。
1.3.2 介紹 lambda, 不過我覺得 the roots of lisp 解釋的比較好。
(lambda (x) (+ x 4)) 這個是 function。
((lambda (x) (+ x 4)) 5) => 9 這叫 function call。
5 是 argument (引數), x 是 parameter (參數), (+ x 4) 是 function body。
lambda 建立的是一個沒有名字的函數, 要使用這個函數就要用上述的那個語法。配合上 define, 就可以將這個 function 對應到一個名字。
也可以寫成
這是一種語法糖衣。
let 也是一種 lambda 的語法糖衣。
let 的功能, 可以由 lambda 來提供。
1.3.3 將一個 procedure 當作參數傳給一個 procedure
1.3.4 傳回一個 procedure, 這還真難懂, 花了我好些時間。
(define (average-damp f) (lambda (x) (average x (f x))))
真的很難懂吧!
first-class elements:
- They may be named by variables.
- They may be passed as arguments to procedures.
- They may be returned as the results of procedures.
- They may be included in data structures.65
符合這些條件的 function 就稱為 first-class function, 又是一個流行的名詞。當然符合這些條件的 object 就是 first-class object, 當然符合這些條件的 people 就是 first-class people XD
這是由 the British computer scientist Christopher Strachey (1916-1975) 幫我們整理出來的概念。
2.1.1 p56 介紹了 pair, 有相關的操作函數 cons, car, cdr, list (p66, 為了方便使用 cons, scheme 提供了 list) p57 最下面的註解解釋了 cons, car (Contents of Address part of Register, cdr (Contents fo Decrement part of Register) 這幾個奇怪的名稱。
2.1.3 p61 實作了 cons, car, cdr。
(define (cons x y)
(define (dispatch m)
(cond ((= m 0) x)
((= m 1) y)
(else (error "Argument not 0 or 1 -- CONS" m))))
dispatch)
(define (car z) (z 0))
(define (cdr z) (z 1))
(car (cons x y))
(cdr (cons x y))
cons 會傳回一個 procedure, 就是 dispatch, 再用來和 car, cdr 一起運作。
2.2.1 p67 提到 (cadr [arg]) = (car (cdr [arg])) 這樣的縮寫方式, 免得寫很多 car, cdr ...
2.2 Hierarchical Data and the Closure Property, 這個 Closure 不是你想的那種, 這是指這個資料結構可以一直的串下去。
cons 最主要就是一個 pair 結構, (cons 1 (cons 2 (cons 3 '() ))) 這樣的資料結構就是由這些 pair 一個一個串接起來, 別小看這麼簡單的東西, 這除了可以表達很直觀的 sequence 結構外, 還可以看做是一顆樹的結構。在 3.3.2 還會用這個來表示 queue, 用這樣的方式:
很難懂, 我花了一天才搞懂這個 queue 的表達方式。
ref: http://descent-incoming.blogspot.tw/2014/08/implement-queue-by-mit-scheme.html
2.3.3 以集合 (sets) 為範例, 以 list (排序和未排序兩種 list) 和 tree 為資料結構, 實作加入一個元素到集合裡頭, 某個元素是否在這個集合, 找出差集和聯集。
scheme 當然沒有類似 c 的 struct 這種東西, 書中用 list 來模擬 tree。
2.4.3 有提到一個 message passing 的設計方式。
3.1 這節在描述 assginment 和 local 變數的行為。
3.1.1 導入了 set! 用來做 assginment。
3.1.3 說明了 functional programming 和 imperative programming。
相信你看得一頭霧水。來看 sicp 3.1.1 的解釋:
函數式程式設計原來就是這樣定義, 之前都搞不懂。網路有些文章還寫像數學函數那樣來寫程式 (大概都是抄 wiki 的吧), 數學函式我知道, 但還是不知道什麼是函數式程式。看些討論講得好神, 不過基本就是這樣。而採用 assignments 的稱為 imperative programming。沒用 assignments 的就是函數式程式設計。
不過我覺得書上提到的 assignment 比較像是使用 c 的 static variable 作 assignment 的動作。
難怪在這前兩章的範例程式時, 就覺得哪裡怪怪的, 沒錯, 我都沒看到 assignment 的概念。原來沒 assignment 也是可以寫程式的。
自制編程語言 page 7 寫的更直接: 「變數值無法被更改」的語言就是函數式編程語言。我覺得這更容易理解, 一句話道出 functional programdming 真諦。
上述程式碼便是分別展示這兩種編程方式。
3.2 在說明引進 assignment 之後, 整個程式的運作模型。很重要, 也很難理解, 我花了不少時間在這邊。
3.2.1 說明了當定義一個 procedure (在 scheme 只能用 lambda 產生一個 procedure), 或使用 define (這只是一個語法糖衣, 實際是用 lambda 產生一個 procedure) 產生一個 procedure 時, 怎麼將這個 procedure 和該名字關聯在一個環境裡。而這個 procedure object 包含兩部份, 一個是 procedure code 本身 (含型式參數部份), 一個是環境 (可能是一個指到某個環境的指標)。
在呼叫這個 procedure 時, 傳入的真正參數是怎麼和 procedure 的參數產生關聯, 這時後會建立一個新的環境/frame, 而呼叫這個 procedure 的環境便成為這個新環境的上層環境。
這節一開始我並沒有完全看懂, 而是看懂 4.1 metacircular evaluator source code 這部份的程式碼時, 我回頭看了好幾次才看懂。也因為 bind 被翻譯成約束, 有些句子也成為閱讀障礙, 建議對照原文, 應該會比較清楚。
雖然翻譯有些問題, 但我還是覺得中文版本幫上不少忙, 當然有原文對照會比較好些。若是沒有中文版本, 我閱讀的速度絕對不能這麼快。
這裡的解釋很重要, 是變數代換和 procedure 求值的關鍵, 若真看不懂, 只好看 4.1 metacircular evaluator source code。兩相對照, 相得益彰。
關鍵點有兩個:
- evaluate 一個 procedure 時, 建立 procedure object (一個 pair, 其中是 function body, 另一個是環境指標, 指到某個環境), 環境指標的上層環境是誰也要清楚。
- apply 一個 procedure 時, 環境是怎麼建立 (會建立一個新環境), 上層環境是誰? 如何將變數值對應到函式的參數。
(define (square x) (* x x)) 這是 evaluate
(square 5) 這是 apply
3.2.2, 3.2.3 3.2.4 以三個實際的程式碼, 分別說明了, 環境如何建立, 對應的變數如何與實際的變數值對應起來, 不算好懂, 得花點腦力才行。不過這個觀念很重要, 可以得知變數是怎麼關聯起來的, 為什麼同名稱的變數不會互相干擾, 獨立對應到不同的值。
要搞懂 interpreter, 3.2 這部份的東西要有清楚的認知。
3.2.3 的例子:
說明環境怎麼用來維護 balance 這個局部變數, 有點像是 c 的 static 變數。
3.3.1 則介紹了 set-car!, set-cdr! 來改變一個 list。
3.4 concurrency: Time is of the essence 提出撰寫 concurrency 程式的問題, 以及利用 mutex 來處理共享資源。在目前這麼流行的 thread 環境中, 這些都是大家朗朗上口的概念, 但這本書可是很早就討論了這些東西, 而就算大家朗朗上口這些概念, 把 thread 程式寫好也不是件容易的事。
chapter4 是很精彩的一章, 4.1 要寫出一個 metacircular evaluator, 講白話就是用 lisp 寫出一個 lisp interpreter, 這裡的實作當然是 scheme。基本上只要讀到 4.1.4 就有個可以執行 scheme 的 interpreter, 是不是很興奮阿! 這可說是我讀這本書的最大動力了, 好久好久以前就想知道 interpreter 是怎麼做的。
如果你覺得 chapter 1, 2, 3 已經很難, 那我得告訴你, 這章更難, 得要有花更多時間的心理準備, 不過好消息是: 這章不是最難的, 後面那章更難。
我飛快閱讀過前面三章, 略過習題, 就是為了可以儘快看到這章。我把精力花在 4.1.1 ~ 4.1.4, 並以 c++ 實作了相同的版本。也許這樣講誇張了點, 光讀這四小結的時間可抵上 1, 2 章。
只要讀完這四個小節, 就可以寫出四則運算、自訂變數、函式呼叫、判斷式的直譯器, The unix programming environment 可是用了一章 (第八章) 在講這些東西, 應該是最簡單的文件了。不過我得先告訴你, 我可是花了至少一個月的時間才真的搞懂, 研究直譯器真的很有趣, 但真的很花時間、精力。
4.1.5
(eval '(* 5 5) user-initial-environment)
(eval (cons '* (list 5 5)) user-initial-environment)
eval 需要帶入一個環境
4.1.6 提到了 internal definitions, 看以下的程式碼:
雖然在 even? 內用到 odd?, 但這應該難不倒你, 這個程式可以正常執行。
這個呢? (f 10) 會得到 20 還是 16 還是發出錯誤訊息? 難倒你了吧? 在 mit-scheme 是發出錯誤訊息, 我也喜歡這樣的實作方式。
4.4 引入了 logic programming, 我對這個主題很陌生, 有了 functional programming 還有個 logic programming, 4.4 給了一個例子說明什麼是 logic programming。不過你以為這樣就結束了嗎? 4.4.* 計劃要打造出一個 logic programming 的 interpreter, 哇 ... 一定要這麼猛嗎? 我快要投降了。
chapter 5 則更進一步, 打造一台模擬機器, 讓你在上頭跑 scheme 的程式。和 java 的 virtual machine (stack base) 不同, sicp 介紹的是 register base machine。這章也是要燃燒很多的腦力, 難度比第四章略難。
5.1 介紹了計算 GCD, factorial 的機器設計流程。
5.1.1 用了一個描述暫存器機器的語言, 如下:
這是用來描述一個計算 GCD 的機器, 只要把暫存器 a, b 存入兩個數字, 機器運算後, 會在暫存器 a 得到這兩個數的 GCD。
5.1.4 提到使用 stack 來實作 recursive。用了 gcd, factorial 來說明這兩個 recursive 的不同。
gcd 只是很單純將原來的問題簡化為另外的 gcd 計算; 而 factorial 則需要計算出另外的 (n-1)!, 並需要這個 (n-1) 的結果。gcd 的計算不需要用上 stack 來支援 recursive, factorial 則需要 stack 來支援 recursive。這個小節最後還以 fibonacci 來說明 double recursion 的概念。
5.2 實作模擬器程式碼, 跑一下哪個 GCD 機器。程式碼難到嚇人, 我這 scheme 初學者完敗在 5.2.2 的 extract-labels, recursive callback function 真的難倒我。
看懂這段程式碼後, 我寫了一篇文章: recursive 加上 callback function
透過程式碼了解其基本精神後, 我大概知道要怎麼寫一個類似 qemu 的模擬器了。
5.4 將寫出一個 explicit-control evaluator, 用來執行 4.1 的那個 metacircular evaluator, 真是太酷了!
有一個 scheme chip, 可參考:
Batali, J. et al. "The Scheme-81 Architecture—System and Chip." Proc. Conference on Advanced Research in VLSI, P. Penfield Jr. ed., MIT, 1982, pp. 69-77.
不過我找不到文章內容。
5.5 提到編譯這個主題, 和 5.4 的 interpreter 對照, 兩個不同的執行方式, 可以體會其中的優缺點。
5.5.7 會示範如何將 compile 後的 code 載入到模擬的機器執行。
這章一樣是要花費很大的時間、腦力, 本科系的朋友應該會很喜歡這些主題。
網路上有很多本書的討論訊息, 但如果你沒看過這本書, 應該還是不知道這本書在說什麼 (我認為是他們沒有完全讀完這本書, 再強調一次, 本書我覺得不好讀, 相當接近我讀的 os 書籍難度; 如果你聽過程式設計師的自我修養 - 連結、載入、程式庫這本書, 我覺得 sicp 比這本還難讀, 自我修養我覺得不算難讀), 希望這篇文章能讓你了解個大概, 也能吸引你去讀這本書。當然沒有哪本書是一定非讀不可, 你總是可以從其他地方或取相同的知識, 要得到本書的知識也並非要從本書開始。網路評價很好的書籍也並非就一定要讀。
這兩篇評論寫的很好, 一看就知道是有看過整本書的人所寫:
1996 的書過時了嗎? 我不認為, 光是第四章、第五章就值回票價。你去哪找兩章就可以講解/完成一個 interpreter 和 register base machine 的書。
相關資料
nil cannot work: http://stackoverflow.com/questions/9115703/null-value-in-mit-scheme使用 '() 來代替 nil
ref:
- SICP读书笔记 (1) : http://kidneyball.iteye.com/blog/922953
- 老赵书托 (2): 计算机程序的构造与解释
- Recursive Functions of Symbolic Expressions and Their Computation by Machine, Part I by John McCarthy:
html, pdf, pdf2. - sicp code: http://mitpress.mit.edu/sicp/code/index.html
- 有一段 Alan J. Perlis 文章的翻譯。
- SICP in Python
- SICP 解题集
- sicp epub version
- 線上課程
http://v.youku.com/v_show/id_XNTMxODY1NTg4.html
這是課程的其中一部分, 有興趣的朋友可自行搜尋其他部份。
沒有留言:
張貼留言
使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。
我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。