- 計算機程序的構造和解釋(JavaScript版)
- (美)哈羅德·阿貝爾森等
- 2304字
- 2024-06-06 19:10:14
1.1.8 函數作為黑箱抽象
sqrt是我們用一組手工定義的函數實現計算過程的第一個例子。請注意,sqrt_iter的聲明是遞歸的,也就是說,該函數的定義基于它自身。基于一個函數自身來定義它的想法可能令人感到不安,這種“循環”定義有意義的理由不清晰:它是否完全地刻畫了一個能由計算機實現的計算過程呢?我們將在1.2節更深入地討論這一問題。現在我們先看看sqrt實例表現的另外一些重要情況。
可以看到,平方根的計算問題可以自然地分解為若干子問題:怎么才能說一個猜測足夠好,怎樣去改進一個猜測等。這些工作中的每一個都通過一個獨立函數完成。整個sqrt程序可以看作一簇函數(如圖1.2所示),它們直接反映了從原問題到子問題的分解。

圖1.2 sqrt程序的函數分解
這一分解策略的重要性,不僅在于把一個問題分解成幾個部分。當然,我們總可以拿來一個大程序,把它切分成幾個部分——最前面10行,隨后10行,再后面10行等。其實,這里的關鍵是,分解得到的每個函數完成了一件可以清晰說明的工作,這使它們可以被用作定義其他函數的模塊。例如,當我們基于square定義函數is_good_enough時,就是把square看作“黑箱”。我們暫時不關心這個函數如何得到結果,而是只注意它能計算平方值的事實。如何計算平方的細節被隱去不提,推遲到以后再考慮。確實如此,如果只看is_good_enough函數,與其說square是函數,不如說它是一個函數的抽象,即所謂函數抽象。在這一抽象層次上,任何計算平方的函數都同樣可以使用。
這樣,如果只考慮返回值,下面這兩個求數值平方的函數并無差別。兩者都以一個數值作為參數,產生這個數的平方作為函數值[22]。

由此可見,一個函數應該能隱藏一些細節。這使該函數的使用者有可能不必自己寫這些函數,而是從其他程序員那里作為黑箱接受它。在使用一個函數時,用戶應該不需要知道它是如何實現的。
局部名
函數的用戶不必關心的實現細節之一,就是實現者為函數所選用的形式參數的名字,也就是說,下面兩個函數應該是不可區分的:

這一原則(函數的意義不依賴于其作者為形式參數選用的名字)從表面看是自明的,但其影響卻很深遠。最直接的影響是,函數的形式參數名必須局部于有關的函數體。例如,我們在前面平方根函數的is_good_enough聲明里用到名字square:

is_good_enough作者的意圖是確定第一個參數的平方是否位于第二個參數的給定誤差的范圍內。可以看到,is_good_enough的作者用名字guess表示其第一個參數,用x表示第二個參數,而square的實際參數就是guess。如果square的作者也用x(例中確實如此)表示參數,可以想到,is_good_enough里的x必須與square里的x不同。運行函數square絕不應該影響is_good_enough里用的那個x的值,因為在square完成計算后,is_good_enough可能還需要x的值。
如果參數不是局部于它們所在的函數體,那么square里的參數x就會與is_good_enough里的參數x相互干擾。如果這樣,is_good_enough的行為方式就依賴我們所用的square的具體版本,square也就不是我們希望的黑箱了。
函數的形式參數在函數聲明里扮演著非常特殊的角色,形式參數的具體名字其實并不重要,這樣的名字稱為是約束的。因此我們說,函數聲明約束了它的所有形式參數。如果在一個函數聲明里把某個約束名統一換名,該函數聲明的意義不變[23]。如果一個名字不是約束的,我們就說它是自由的。一個名字的聲明被約束的那一集語句稱為這個名字的作用域。在一個函數聲明里,被聲明為函數形式參數的約束名都以該函數的體為作用域。
在上面is_good_enough的聲明里,guess和x是約束的名字,而abs和square是自由的。要保證is_good_enough的意義與我們對名字guess和x的選擇無關,只需要求這些名字都與abs和square不同(如果我們把guess重命名為abs,就會因為捕獲了名字abs而引進一個錯誤,因為這樣做把一個原來自由的名字變成約束的了)。is_good_enough的意義當然與其中自由名字的選擇有關,這個意義顯然依賴于(這個聲明之外的)一些事實:名字abs引用一個函數,該函數能求出數值的絕對值。如果我們把is_good_enough聲明里的abs換成math_cos(基本余弦函數),它計算的就是另一個不同的函數了。
內部聲明和塊結構
到現在為止,我們只分離出了一種可用的名字:函數的形式參數是其函數體里的局部名字。這個平方根程序還表現了另一種情況,我們也會希望能控制其中名字的使用。目前這個程序由幾個相互獨立的函數組成:

問題是,在這個程序里,只有一個函數對sqrt的用戶是重要的,那就是這里的sqrt。其他函數(sqrt_iter、is_good_enough和improve)只會干擾用戶的思維,因為在需要與平方根程序一起使用的其他程序里,它們再不能聲明另一個名為is_good_enough的函數作為程序的一部分了,因為在sqrt里用了這個名字。當許多分別工作的程序員一起構造大型系統時,這一問題就會變得非常嚴重。舉例說,在構造一個大型數值函數庫時,許多數值函數都需要計算一系列近似值,因此它們都可能需要名字為is_good_enough和improve的函數作為輔助函數。由于這些情況,我們自然會希望把子函數也局部化,把它們隱藏到sqrt里面,使sqrt可以與其他同樣采用逐步逼近方法的函數共存,即使它們中每一個都有自己的is_good_enough函數。為使這件事成為可能,我們允許在一個函數聲明里包含一些局部于這個函數的內部聲明。例如,在解決平方根問題時,我們可以寫:


任意一對匹配的花括號表示一個塊,塊內部的聲明局部于這個塊。這種嵌套的聲明結構稱為塊結構,這是最簡單的名字包裝問題的正確處理方法。實際上,這里還潛藏著一個很好的情況:除了可以內部化輔助函數的聲明,我們還可能簡化它們。因為x在sqrt的聲明中是受約束的,函數is_good_enough、improve和sqrt_iter也都聲明在sqrt內部,也就是在x的定義域里。這樣,明確地把x在這些函數之間傳來傳去就沒有必要了。我們可以讓x作為這些內部聲明里的自由變量,如下所示。這樣,在外圍的sqrt被調用時,x由其實際參數得到自己的值。這種做法稱為詞法作用域[24]。

我們將廣泛使用塊結構,借助它把大程序分解為一些容易掌握的片段[25]。塊結構的思想源自程序設計語言Algol 60,這種結構也出現在各種最新的程序設計語言里,是幫助我們組織大程序的結構的一種重要工具。