Haskell/真值
在上一章中,我們使用等號在 Haskell 中定義變數和函式,如下面的程式碼所示
r = 5
這意味著程式的評估將用 5 替換 r 的所有出現(在定義的範圍內)。類似地,評估程式碼
f x = x + 3
將用該數字加 3 替換所有以 f 後跟一個數字(f 的引數)出現的 f。
數學也使用等號,但其方式重要且微妙地不同。例如,考慮以下簡單問題
示例:求解以下方程
我們在這裡關注的不是將值表示為,反之亦然。相反,我們將方程讀作一個命題,即某個數字加 3 後得到的結果為 5。求解方程意味著找出哪個值(如果有的話)的值使該命題為真。在本例中,初等代數告訴我們(即 2 是使方程為真的數字,得到)。
比較值以檢視它們是否相等在程式設計中也很有用。在 Haskell 中,此類測試看起來就像一個方程。由於等號已用於定義事物,因此 Haskell 使用雙等號 == 代替。在 GHCi 中輸入我們上面的命題
Prelude> 2 + 3 == 5 True
GHCi 返回 "True",因為等於 5。如果我們使用一個不成立的方程呢?
Prelude> 7 + 3 == 5 False
簡潔明瞭。接下來,我們將在此類測試中使用我們自己的函式。讓我們嘗試本章開頭提到的函式 f
Prelude> let f x = x + 3 Prelude> f 2 == 5 True
這按預期工作,因為 f 2 評估為 2 + 3。
我們還可以比較兩個數值以檢視哪個值更大。Haskell 提供了一些測試,包括:<(小於)、>(大於)、<=(小於或等於)和 >=(大於或等於)。這些測試與 ==(等於)的工作原理類似。例如,我們可以使用 < 以及上一模組中的 area 函式來檢視特定半徑的圓的面積是否小於某個值。
Prelude> let area r = pi * r ^ 2 Prelude> area 5 < 50 False
GHCi 在確定這些算術命題是真還是假時,究竟發生了什麼?考慮一個不同但相關的議題。如果我們在 GHCi 中輸入一個算術表示式,該表示式將被評估,結果的數值將顯示在螢幕上
Prelude> 2 + 2 4
如果我們將算術表示式替換為相等比較,則似乎也發生了類似的事情
Prelude> 2 == 2 True
雖然之前返回的 "4" 是代表某種計數、數量等的數字,但 "True" 是代表命題真值的值。此類值稱為真值或布林值。[1] 自然地,只存在兩個可能的布林值:True 和 False。
True 和 False 是真實的值,而不僅僅是類比。布林值在 Haskell 中與數值具有相同的身份,您可以以類似的方式操作它們。一個簡單的例子
Prelude> True == True True Prelude> True == False False
True 確實等於 True,而 True 不等於 False。現在:你能回答 2 是否等於 True 嗎?
Prelude> 2 == True
<interactive>:1:0:
No instance for (Num Bool)
arising from the literal ‘2’ at <interactive>:1:0
Possible fix: add an instance declaration for (Num Bool)
In the first argument of ‘(==)’, namely ‘2’
In the expression: 2 == True
In an equation for ‘it’: it = 2 == True
錯誤!這個問題毫無意義。我們不能將數字與非數字或布林值與非布林值進行比較。Haskell 嵌入了該概念,並且難看的錯誤訊息對此進行了抱怨。忽略大部分雜亂,該訊息指出左側存在一個數字(Num),因此右側應該期望某種數字;但是,布林值(Bool)不是數字,因此相等性測試失敗。
因此,值具有型別,而這些型別定義了我們可以或不能對值執行的操作的限制。True和False是型別為Bool的值。2很複雜,因為有許多不同型別的數字,所以我們將在以後解釋。總的來說,型別提供了強大的功能,因為它們使用有意義的規則來規範值的行為,從而更容易編寫執行正確的程式。我們將在以後多次回到型別主題,因為它們對Haskell非常重要。
像2 == 2這樣的相等性測試是一個表示式,就像2 + 2一樣;它以幾乎相同的方式計算為一個值。我們在上一個例子中遇到的醜陋錯誤訊息也是這樣說的。
In the expression: 2 == True
當我們在提示符中輸入2 == 2,GHCi“回答”True時,它只是在計算一個表示式。事實上,==本身是一個函式,它接受兩個引數(分別是相等性測試的左側和右側),但語法值得注意:Haskell 允許將二元函式寫成中綴運算子,置於其引數之間。當函式名只使用非字母數字字元時,這種中綴方法是常見的用例。如果您希望以“標準”方式使用這樣的函式(在引數之前寫函式名,作為一個字首運算子),則函式名必須用括號括起來。因此,以下表達式是完全等價的。
Prelude> 4 + 9 == 13 True Prelude> (==) (4 + 9) 13 True
因此,我們看到了(==)如何像前一個模組中的areaRect一樣作為函式工作。相同的考慮適用於我們提到的其他關係運算符(<、>、<=、>=)和算術運算子(+、*等)——它們都是接受兩個引數並通常寫成中綴運算子的函式。
一般來說,我們可以說 Haskell 中的具體事物要麼是值,要麼是函式。
Haskell 提供了三個基本函式,用於進一步操作邏輯命題中的真值。
(&&)執行與操作。給定兩個布林值,如果第一個和第二個都是True,它計算為True,否則計算為False。
Prelude> (3 < 8) && (False == False) True Prelude> (&&) (6 <= 5) (1 == 1) False
(||)執行或操作。給定兩個布林值,如果至少其中一個是True,它計算為True,否則計算為False。
Prelude> (2 + 2 == 5) || (2 > 0) True Prelude> (||) (18 == 17) (9 >= 11) False
not執行布林值的否定;也就是說,它將True轉換為False,反之亦然。
Prelude> not (5 * 2 == 10) False
Haskell 庫已經包含了不等於的關係運算符函式(/=),但我們可以很容易地自己實現它,方法是
x /= y = not (x == y)
注意,我們甚至可以在定義運算子時將它們寫成中綴形式。全新的運算子也可以從 ASCII 符號中建立(這意味著主要是鍵盤上使用的常見符號)。
Haskell 程式經常使用布林運算子來實現便捷的縮寫語法。當相同的邏輯用不同的風格書寫時,我們稱之為語法糖,因為它從人的角度來看使程式碼變得更甜。我們將從守衛開始,這是一個依賴於布林值並允許我們編寫簡單但功能強大的函式的功能。
讓我們實現絕對值函式。實數的絕對值是丟棄符號後的數字;所以如果數字為負(即小於零),則符號被反轉;否則它保持不變。我們可以將定義寫成
在這裡,用於計算的實際表示式取決於對做出的命題集。如果為真,則我們使用第一個表示式,但如果為真,則我們使用第二個表示式。為了用守衛在 Haskell 中表達這個決策過程,實現可能如下所示:[2]
示例:絕對值函式。
absolute x
| x < 0 = -x
| otherwise = x
值得注意的是,上面的程式碼與相應的數學定義幾乎一樣易讀。讓我們剖析定義的組成部分。
- 我們像普通函式定義一樣開始,為函式提供一個名稱,
absolute,並說明它將接受一個引數,我們將其命名為x。
- 我們不是直接用
=和定義的右側來進行,而是將兩個備選方案分別放在單獨的行上。[3] 這些備選方案是真正的守衛。注意,空白(第二行和第三行的縮排)不僅僅是為了美觀;它是程式碼被正確解析所必需的。
- 每個守衛都以管道字元
|開頭。在管道字元之後,我們放一個計算結果為布林值的表示式(也稱為布林條件或謂詞),後面跟著定義的其餘部分。只有當謂詞計算結果為True時,函式才會使用等於符號和來自該行的右側。
otherwise情況用於當所有前面的謂詞都沒有計算為True時。在這種情況下,如果x不小於零,它一定大於或等於零,所以最後的謂詞也可以簡單地是x >= 0;但otherwise也能正常工作。
注意
otherwise背後沒有語法上的魔力。它與 Haskell 的預設變數和函式一起定義,僅僅是
otherwise = True
這個定義使otherwise成為一個萬能的守衛。由於對守衛謂詞的計算是順序進行的,因此只有在所有前面的情況都沒有計算為True時,才會到達otherwise謂詞(所以確保你始終將otherwise放在最後一個守衛位置!)。通常,最好始終提供otherwise守衛,因為如果對於某些輸入,所有謂詞都沒有為真,就會產生一個相當醜陋的執行時錯誤。
注意
在第一個守衛中
| x < 0 = -x
我們使用減號來否定x。值得注意的是,這種表達符號反轉的方式是一種特殊情況,因為-不是一個接受一個引數並計算為0 - x的函式,而僅僅是一個語法縮寫。雖然這種縮寫非常方便,但它偶爾會與(-)作為實際函式(減法運算子)的用法發生衝突,這可能是一個潛在的煩惱來源(例如,嘗試在不使用任何括號的情況下寫出三個負四)。
因此,當用負數測試absolute時,你應該像這樣呼叫它
Prelude> absolute (-10) 10
where 語句與 guards 配合得很好。例如,考慮一個計算二次方程 的唯一(實數)解數量的函式。
numOfRealSolutions a b c
| disc > 0 = 2
| disc == 0 = 1
| otherwise = 0
where
disc = b^2 - 4*a*c
where 定義在所有 guards 的範圍內,這使我們不必重複disc的表示式。
註釋
- ↑ 這個詞是對數學家和哲學家 喬治·布林 的致敬。
- ↑ Haskell 已經提供了這個函式,名為
abs,所以在現實世界中,你不需要自己提供實現。 - ↑ 我們可以將這些行連線起來,並將所有內容都寫在一行中,但這會降低可讀性。