程式設計科學/Guzzintas 和其他密碼
雖然我們現在有一個不錯的表示常量的方法,但我們現在面臨一個更大的問題:如何組合項和常量。我們不能使用 Sway 的+運算子,因為它只適用於數字和字串。[1] 相反,我們將建立一個物件來儲存要新增的兩個專案。[2] 讓我們建立一個名為 plus 的建構函式,以提醒我們它生成一個儲存兩個物件(潛在)總和的物件
function plus(p,q) //p and q are terms/constants
{
this;
}
但是,除了提取繫結到 p 和 q 的原始專案外,我們無法對 plus 物件做太多事情。
我們應該如何增強我們的 plus 建構函式?就像項和常量一樣,plus 物件應該能夠
* compute its value * compute its derivative * visualize itself
當然,plus 物件不知道如何執行任何這些操作,因為它只是一個用於儲存兩個專案的容器。但是,它可以做的是要求這些專案執行計算,然後以有意義的方式組合結果。[3]
現在我們的問題變成了
- 專案的值應該如何組合?
- 專案的微分應該如何組合?
- 專案的視覺化應該如何組合?
對於value 方法,p 的值和q 的值應該如何組合?由於 p 的值是一個數字,q 的值是一個數字,並且由於 plus 物件表示其專案的加法,我們可以簡單地將專案值加在一起
function plus(p,q) //p and q are terms/constants
{
function value(x)
{
p . value(x) + q . value(x);
}
this;
}
因此,要計算兩個加在一起的項的y 值,我們只需分別找到項的y 值,然後將結果加在一起。在數學上,
讓我們測試一下
sway> var a = term(-5,2); sway> var b = term(7,0); sway> a . value(3) + b . value(3); INTEGER: -38 sway> var z = plus(a,b); sway> z . value(3); INTEGER: -38
請注意,我們的 plus 建構函式實際上沒有指定形式引數 p 和 q 必須繫結到的物件型別;唯一的要求是這些物件有一個接受單個引數的 value 方法。我們將在後面利用這一事實,並使用 plus 物件將 terms、constants 和其他 plus 物件隨意粘合在一起。
冪法則和求和
[edit | edit source]現在我們將注意力轉向 plus 物件需要實現的第二個方法 diff。當然,冪法則不適用於求和,因為該法則只適用於計算單個項的導數。
事實證明,兩個加在一起的東西的導數是每個東西單獨的導數的總和。在數學上,
現在這裡是最棘手的部分(現在看起來很棘手,但過不了多久,你會覺得這就像切蛋糕一樣簡單)。我們使用 plus 物件來表示兩個項的加法,對嗎?上面的等式的右側涉及加法。正在加什麼?兩個導數。如果 a 和 b 是項物件,那麼 a 和 b 的導數是什麼樣的東西。兩者都是項!因此在這種情況下,右側僅僅是兩個項的總和。但是,我們用什麼來表示兩個項的總和......等等......一個 plus 物件。
哇!然後,一個由兩個項組成的 plus 物件的導數是另一個包含這兩個項的導數的 plus 物件。這既強大又簡單。
如果上面的解釋讓你感到困惑,也許看一下程式碼會有所幫助。以下是修改後的 powerRule
function powerRule(obj)
{
if (obj is :term)
{
var a = obj . a;
var n = obj . n;
term(a * n,n - 1);
}
else if (obj is :plus)
{
var dp/dx = powerRule(obj . p);
var dq/dx = powerRule(obj . q);
plus(dp/dx,dq/dx);
}
else
{
throw(:calculusError,"powerRule: unknown object");
}
}
請注意,dp/dx 是一個變數名,它與 dp / dx 完全不同,dp / dx 是變數 dp 除以 dx。對於 plus 物件,我們提取兩個項 p 和 q,使用冪法則對它們進行求導,然後將它們組合成一個 plus 物件。
超過兩個項
[edit | edit source]我們的 plus 建構函式非常適合表示直線,因為直線由兩項組成,其中一項是常量。對於三個或更多項的總和,我們該怎麼辦?
如何...什麼也不做。
是的,什麼也不做。事實證明,我們對 plus 的設計如此精妙,它不僅處理了兩個項的總和,而且還處理了 plus 物件和一個項的總和。[4]。回想一下,plus 物件中引數 p 和 q 的唯一要求是它們都繫結到具有 value 方法的物件。plus 物件有 value 方法嗎?是的,的確!所以這意味著 p 或 q 或兩者都可以繫結到 plus 物件。讓我們看看
var a = term(-5,0); var b = plus(term(3,1),a); var y = plus(term(4,2),b);
變數 y 現在是指多項式
或者更簡單地說
現在,僅僅因為我們應該(並且可以)能夠使用 plus 建構函式隨意組合求和和項,並不意味著我們可以假設 plus 按照寫的那樣是完全正確的。我們需要測試[5] 才能徹底說服自己程式碼是有效的。讓我們在 x 的某個易於驗證的值(例如 x = 2)下測試 a、b 和 c
sway> a . value(2); INTEGER: -5 sway> b . value(2); INTEGER: 1 sway> y . value(2); INTEGER: 17
在這個互動中,我們看到 a,它是 ,當 x 的值為 2 時,其值為 -5。物件 b,它是一個表示 的 plus 物件,其值為 6 - 5 或 1。物件 y,它也是一個 plus 物件,表示 ,其值為 16 + 6 - 5 或 17。
物件 a(只是一個項)和物件 b(由兩個項組成)都給出正確答案並不令人驚訝;我們完全按照它們預期的方式使用它們的建構函式。為什麼 y 有效,因為它由一個項和一個 plus 物件組成,這一點就不那麼明顯了。下一節將展示 視覺化,以幫助你理解“為什麼”。
視覺化
[edit | edit source]一個非常好的方法來檢視為什麼 y 有效是視覺化 y 物件。我們首先修改 term 建構函式以新增一個 toString 方法。通常,toString 方法用於生成一個表示物件當前狀態的字串。[6] 這樣的字串稱為視覺化。
在繼續之前,請閱讀有關 字串 的內容。
以下是修改後的 term 函式及其新的 toString 方法
function term(a,n)
{
function value(x) { a * (x ^ n); }
function toString()
{
"" + a + "x^" + n;
}
this;
}
一如既往,我們寫一些程式碼,然後測試、測試、測試!
sway> var t = term(3,2); sway> t . toString(); STRING: "3x^2"
對於任何項,我們都可以快速看到物件的部分,這很容易理解。[7] 這是視覺化的目標。
我們還需要在 plus 中新增一個視覺化
function plus(p,q) //p and q are terms or plus objects
{
function value(x) { p . value(x) + q . value(x); }
function toString()
{
p . toString() + " + " + q . toString();
}
this;
}
請注意,plus 透過使用其組成物件的視覺化來進行視覺化,並新增一個 '+' 符號來表示加法。
現在,我們重新制作 a、b 和 y,並強制執行這些新的 term 和 plus 定義
var a = term(-5,0); var b = plus(term(3,1),a); var y = plus(term(4,2),b);
讓我們看看 y 的樣子
sway> y . toString(); STRING: 4x^2 + 3x^1 + -5x^0
看起來完全符合預期。
為了進一步瞭解為什麼視覺化有效,有時檢視生成結果的呼叫序列會有所幫助。這些呼叫被組織成稱為“呼叫樹”的東西。呼叫中的操作從起始呼叫縮排一個級別。左箭頭表示返回值。
以下是 y 的 toString 方法呼叫的呼叫樹
call y . toString() //object y is plus(term(4,2),b)
call p . toString() //object p is term(4,2)
<-- "4x^2" //return value
" + "
call q . toString() //object q is b, plus(term(3,1),a)
call p . toString() //object p is term(3,1)
<-- "3x^1" //return value
" + "
call q . toString() //object q is a, term(-5,0)
<-- "-5x^0" //return value
<-- 3x^1 + -5x^0 //return value
<-- 4x^2 + 3x^1 + -5x^0 //return value
將字串從上到下組合到第一個縮排級別,我們將獲得整體返回值
4x^2 + 3x^1 + -5x^0
我們還可以構建一個呼叫樹來確定當x = 2 時y的值。
call y . value(2)
call p . value(2) // p is 4x^2
<-- 16
+
call q . value(2) // q is 3x^1 + -5x^0
call p . value(2) // p is 3x^1
<-- 6
+
call q . value(2) // q is -5x^0
<-- -5
<-- 1
<-- 17
將第一級縮排處的返回值相加得到 17,即總的返回值。
視覺化是一個重要的技術。你應該為所有物件都包含視覺化方法,以便在程式出現問題時能夠輕鬆地進行除錯。在這種情況下,你的視覺化可能會指出某個物件沒有按照預期那樣出現,這是解決問題的重要的第一步。
冪法則修改
[edit | edit source]我們已經對項的和測試了我們的powerRule函式,但它對和的和有效嗎?讓我們來檢查一下程式碼
function powerRule(obj)
{
if (obj is :term)
{
var a = obj . a;
var n = obj . n;
term(a * n,n - 1);
}
else if (obj is :plus)
{
var dp/dx = powerRule(obj . p);
var dq/dx = powerRule(obj . q);
plus(dp/dx,dq/dx);
}
else
{
throw(:calculusError,"powerRule: unknown object");
}
}
如果形式引數obj繫結到一個plus物件的和,則powerRule函式會遞迴地呼叫組成這個和的兩個物件。只要這兩個物件中的任何一個都是plus或term物件,似乎如果powerRule能夠處理它。在plus物件的情況下,powerRule會再次遞迴地呼叫自身。
讓我們用繫結到變數y的上面那個多項式來測試powerRule函式。回想一下y的視覺化方式為
4x^2 + 3x^1 + -5x^0
我們希望powerRule函式產生一個多項式,視覺化為類似下面的東西
8x + 3
讓我們來看看。
var dy/dx = powerRule(y); sway> dy/dx . toString(); STRING: 8x^1 + 3x^0 + 0x^-1
觀察結果的最後一項,我們注意到零乘以任何東西都是零,所以結果等價於
8x^1 + 3x^0 + 0
或者
8x^1 + 3x^0
注意,正如之前一樣, 等於 1,結果變為
8x^1 + 3*1
或者
8x^1 + 3
最後,意識到 僅僅是x,結果變為
8x + 3
如預期的那樣。本章末尾的一些問題探討了如何讓plus和term自動執行這些簡化操作。
其他數學組合的項
[edit | edit source]如果我們希望減去兩項,就像這樣
y = 3x^2 - 4x
我們可以編寫一個名為minus的函式來完成這個操作
function minus(p,q)
{
function value(x)
{
p . value(x) - q . value(x);
}
function toString()
{
p . toString() + " - " + q . toString();
}
this;
}
這就像plus一樣,除了在value和toString方法中使用減號而不是加號。[8]
項的減法的冪法則是什麼?它類似於,但不完全相同於項的加法的冪法則
因此,如寫的那樣,powerRule不能用於minus物件,因為powerRule中沒有程式碼來減法或甚至產生minus物件。我們需要在powerRule中新增一個新的子句
function powerRule(obj)
{
if (obj is :term)
{
var a = obj . a;
var n = obj . n;
term(a * n,n - 1);
}
else if (obj is :plus)
{
var dp/dx = powerRule(obj . p);
var dq/dx = powerRule(obj . q);
plus(dp/dx,dq/dx);
}
else if (obj is :minus)
{
var dp/dx = powerRule(obj . p);
var dq/dx = powerRule(obj . q);
minus(dp/dx,dq/dx);
}
else
{
throw(:calculusError,"powerRule: unknown object");
}
}
雖然這將有效,但有一個線索表明我們做錯了什麼。每當你看到一個處理物件的if-chain時,這意味著你沒有充分利用物件的強大功能,你應該重構你的程式碼以刪除if-chain。下一節將展示如何做到這一點。
物件的方式
[edit | edit source]程式的設計通常隨著時間的推移而演變。一些程式設計師對他們編寫的程式碼感到非常投入,即使有更好的方法出現,他們也會頑固地堅持下去。用肯尼·羅傑斯的話來說,“你必須知道何時該抓住,何時該放棄”。在這種情況下,我們的powerRule函式正在變得難以控制,所以我們要放棄,重新開始。
我們的新方法擴充套件了下面的語句
- 一個物件應該能夠...
在我們的term和plus物件的情況下,我們已經實現了上面語句的以下版本
- 一個term物件應該能夠返回它的係數
- 一個term物件應該能夠返回它的指數
- 一個term物件應該能夠計算給定x值的y值
- 一個term物件應該能夠視覺化自身
- 一個plus物件應該能夠返回它的元件
- 一個plus物件應該能夠計算給定x值的y值
- 一個plus物件應該能夠視覺化自身
在這個列表中,我們將新增以下語句
- 一個term物件應該能夠進行微分
- 一個plus物件應該能夠進行微分
- 其他類似物件應該能夠進行微分
讓我們首先更新我們的term建構函式,以便項能夠進行微分。我們透過新增一個diff(用於微分)方法來實現這一點
function term(a,n)
{
function value(x) { ... }
function toString() { ... }
function diff()
{
term(a * n,n - 1);
}
this;
}
diff方法的主體僅僅是powerRule函式中發現的項程式碼的一種形式。
我們也可以對plus建構函式做同樣的事情
function plus(p,q)
{
function value(x) { ... }
function toString() { ... }
function diff()
{
var dp/dx = p . diff();
var dq/dx = q . diff();
plus(dp/dx,dq/dx);
}
this;
}
為了使plus更短一些,我們可以用一行程式碼替換diff方法的主體
function diff()
{
plus(p . diff(),q . diff());
}
利用plus是兩個引數的函式這一事實,我們也可以使用中綴表示法
function diff()
{
p . diff() plus q . diff();
}
修改minus建構函式類似。那麼乘法和除法項呢?
根據 CME,對乘積進行微分的數學規則是
除法的規則更復雜
times和div建構函式的diff方法的實現留作練習。
完成所有這些之後,powerRule函式變得非常簡單
function powerRule(obj)
{
obj . diff();
}
事實上,它太簡單了,它不再做任何有用的工作,可以丟棄了。
問題
[edit | edit source]所有公式都是使用小學算術優先順序寫的。
1. 如果你將一個plus物件傳遞給原始的powerRule函式,會發生什麼?解釋會發生什麼。
2. 定義並測試一個times建構函式。確保新增toString和diff方法。
3. 定義並測試一個div建構函式。確保新增toString和diff方法。
4. 修改term的toString方法,以便如果指數為零,則僅使用係數。
5. 修改term的toString方法,以便如果指數為 1,則忽略係數和/或指數。也就是說,term(3,1) 應該顯示為 3x,而不是 3x^1,而term(1,4) 應該顯示為 x^4,而不是 1x^4。
6. 修改term的toString方法,以便如果係數為零,則產生空字串""。
7. 修改plus建構函式,如果項的係數為零,則將其丟棄。你如何“丟棄”一個項?
8. 修改plus建構函式,以便如果第二個引數是一個係數為負數的項,則它會產生一個適當的minus物件。
9. 使用 Sway 解決 CME 第 64 頁問題 6。
10. 使用 Sway 對 y = (x - 3/2) + (x^2 -5x) + (3x^3 + 7x^2 + 3x + 5) 進行微分。定義函式y,然後
11. 使用紙筆完成 CME 第 64 頁的練習 1-5。
12. 使用紙筆解決 CME 第 77 頁的練習 11。
腳註
[edit | edit source]- ↑ 稍後,我們將學習如何覆蓋+運算子,以便它也可以新增項和常量。
- ↑ 我們為什麼要這樣做並不明顯。隨著我們繼續進行,我們將看到這種方法有效,但它不會給我們太多關於如何一開始就提出這種解決方案的見解。我們本質上是要延遲專案的新增,直到我們真正需要將它們加在一起(例如,當我們試圖計算特定的y值時)。延遲的概念是一個強大的計算機科學概念。然而,知道何時延遲,更像是一種藝術,而不是科學。
- ↑ 物件之間主要存在兩種關係。在本例中,plus 物件與 p(或 q)之間的關係是 客戶關係。物件 p(或 q)被稱為 plus 物件的客戶,因為 p(或 q)是一個元件。物件之間另一種主要關係是 繼承,其中物件共享特徵。你可以在 Sway 參考手冊 中瞭解更多關於繼承的資訊。儘管在本例中沒有使用顯式繼承,但可以認為 項 和 常量 繼承了三種方法的思想:值、差 和 toString。
- ↑ 如果你編寫簡潔優雅的程式碼,你將經常發現這些令人愉快的事件。
- ↑ 當你成為高階程式設計師時,與其測試一些程式碼來檢視它是否看起來正確,你可能會 證明 它的正確性。證明程式碼正確性的優勢在於,有時執行所有可能的測試非常困難或不可能。
- ↑ 使用名稱 toString 只是一個約定。我們以 Java 程式語言為基礎,它使用 toString 方法來實現這個目的。
- ↑ 請注意,這種表示使用你在小學學到的運算子優先順序,即求冪運算先於係數的乘法運算。這與 Sway 不同,Sway 會將表示式 3 * x ^ 2 評估為 (3 * x) ^ 2。要了解更多關於 Sway 如何計算算術表示式的知識,請參閱 Sway 參考手冊中的優先順序和結合性。
- ↑ 透過類比程式設計 的一個很好的例子。請注意,透過複製現有函式並進行少量修改來建立新函式通常是一個 "壞主意"。如果你發現自己正在這樣做,問問自己這兩個函式如何可以合併成一個函式?對於 plus 和 minus,有一種很好的方法可以做到這一點,但為了不影響敘述流程,我們將跳過它。