Haskell/下一步
本章介紹了模式匹配以及兩個新的語法元素:if 表示式和let 繫結。
Haskell 語法支援if... then... else... 形式的普通條件表示式。例如,考慮一個函式,如果它的引數小於0則返回(-1);如果它的引數是0則返回0;如果它的引數大於0則返回1。預定義的signum函式已經完成了這項工作;但為了說明起見,讓我們定義我們自己的版本
示例: signum 函式。
mySignum x =
if x < 0
then -1
else if x > 0
then 1
else 0
您可以嘗試使用這個
*Main> mySignum 5 1 *Main> mySignum 0 0 *Main> mySignum (5 - 10) -1 *Main> mySignum (-1) -1
在最後一個例子中,"-1" 括號是必須的;如果缺少,Haskell 會認為您試圖從mySignum 中減去1(這會導致型別錯誤)。
在 if/then/else 結構中,首先評估條件(在本例中為x < 0)。如果結果為True,則整個結構將評估為then表示式;否則(如果條件為False),則該結構將評估為else表示式。所有這些都非常直觀。但是,如果您以前使用過命令式語言,那麼您可能會驚訝地發現 Haskell 總是要求同時有then 和else子句。該結構必須在兩種情況下都產生一個值,並且特別是在兩種情況下都產生相同型別的值。
使用 if / then / else 的函式定義,如上面的示例,可以使用Guards重寫。
示例: 從 if 到 guards
mySignum x
| x < 0 = -1
| x > 0 = 1
| otherwise = 0
類似地,在Truth values 中定義的絕對值函式可以用 if/then/else 呈現
示例: 從 guards 到 if
absolute x =
if x < 0
then -x
else x
為什麼要使用 if/then/else 而不是 guards?正如您將在後面的示例以及您自己的程式設計中看到的那樣,兩種處理條件的方式在可讀性或便捷性方面可能會有所不同,具體取決於情況。在很多情況下,兩種選擇都能同樣有效。
考慮一個程式,該程式跟蹤來自賽車比賽的統計資料,其中賽車手根據他們在每場比賽中的分類獲得積分,計分規則為
- 第一名獲得 10 分;
- 第二名獲得 6 分;
- 第三名獲得 4 分;
- 第四名獲得 3 分;
- 第五名獲得 2 分;
- 第六名獲得 1 分;
- 其他賽車手沒有積分。
我們可以編寫一個簡單的函式,它接收一個分類(用一個整數表示:1 表示第一名等[1])並返回獲得的積分。一個可能的解決方案使用 if/then/else
示例: 使用 if/then/else 計算積分
pts :: Int -> Int
pts x =
if x == 1
then 10
else if x == 2
then 6
else if x == 3
then 4
else if x == 4
then 3
else if x == 5
then 2
else if x == 6
then 1
else 0
太糟糕了!不可否認,如果我們使用 guards 而不是 if/then/else,它看起來不會這麼難看,但編寫(和閱讀!)所有這些相等性測試仍然很乏味。不過,我們可以做得更好
示例: 使用分段定義計算積分
pts :: Int -> Int
pts 1 = 10
pts 2 = 6
pts 3 = 4
pts 4 = 3
pts 5 = 2
pts 6 = 1
pts _ = 0
好多了。但是,即使以這種方式定義pts(我們現在將其任意地稱為分段定義)以清晰的方式向程式碼閱讀者展示了該函式的作用,但考慮到我們迄今為止看到的 Haskell,語法看起來很奇怪。為什麼pts 有七個等式?這些數字在它們的左側在做什麼?變數引數呢?
Haskell 的這個特性稱為模式匹配。當我們呼叫pts 時,引數會與每個等式左側的數字進行匹配,而這些數字 wiederum 是模式。匹配是按照我們編寫等式的順序進行的。首先,引數與第一個等式中的1 進行匹配。如果引數確實是1,則我們匹配成功,並且使用第一個等式;因此pts 1 將如預期的那樣評估為10。否則,將按照相同的方式順序嘗試其他等式。然而,最後一個等式有點不同:_ 是一個特殊的模式,通常稱為“萬用字元”,可以理解為“任何東西”:它與任何東西都匹配;因此,如果引數與任何以前的模式都不匹配,則pts 將返回零。
至於缺少x 或任何其他代表引數的變數,我們根本不需要它來編寫定義。所有可能的返回值都是常量。此外,變數用於在定義的右側表達關係,因此在我們的pts 函式中,x 是不必要的。
但是,我們可以使用一個變數來使pts 更簡潔。給予賽車手的積分從第三名到第六名按每位置一分的速度規律遞減。注意到這一點後,我們可以消除七個等式中的三個,如下所示
示例: 混合樣式
pts :: Int -> Int
pts 1 = 10
pts 2 = 6
pts x
| x <= 6 = 7 - x
| otherwise = 0
所以,我們可以混合兩種定義風格。事實上,當我們在等式的左側編寫pts x 時,我們也使用模式匹配!作為模式,x(或任何其他變數名)與_ 一樣匹配任何東西;唯一的區別是它還為我們提供了在右側使用的名稱(在本例中,這是編寫7 - x 所必需的)。
| 練習 |
|---|
從第二版pts 到第三版,我們有點作弊:它們並不完全相同。你能發現區別是什麼嗎? |
除了整數,模式匹配還可以使用各種其他型別的值。一個方便的例子是布林值。例如,我們在Truth values 中遇到的(||) 邏輯或運算子可以定義為
示例: (||)
(||) :: Bool -> Bool -> Bool
False || False = False
_ || _ = True
或者
示例: (||),另一種方式
(||) :: Bool -> Bool -> Bool
True || _ = True
False || y = y
當同時匹配兩個或多個引數時,只有當所有引數都匹配時,才會使用該等式。
現在,讓我們討論使用模式匹配時可能出現的一些問題
- 如果我們將一個匹配任何東西的模式(例如每個
pts示例中的最後一個模式)放在更具體的模式之前,則後者將被忽略。GHC(i) 通常會在這種情況下警告我們“模式匹配重疊”。 - 如果沒有模式匹配成功,則會觸發錯誤。通常,最好確保模式涵蓋所有情況,就像
otherwiseguard 不是必需的,但強烈建議使用一樣。 - 最後,雖然您可以嘗試使用各種方法(重新)定義
(&&)[2],但這裡有一個版本不能正常工作
(&&) :: Bool -> Bool -> Bool
x && x = x -- oops!
_ && _ = False
- 該程式不會測試引數是否相等,僅僅因為我們碰巧對兩個引數使用了相同的名稱。就匹配而言,我們也可以在第一種情況下編寫
_ && _。更糟糕的是:因為我們對兩個引數都使用了相同的名稱,所以 GHC(i) 由於“`x` 的衝突定義”而拒絕該函式。
雖然上面的例子表明模式匹配有助於編寫更優雅的程式碼,但這並不能解釋它為什麼如此重要。因此,讓我們考慮編寫fst 定義的問題,該函式提取一對中的第一個元素。在這一點上,這似乎是不可能的任務,因為訪問一對第一個值的唯一方法是使用fst 本身...但是,以下函式執行與fst 相同的操作(在 GHCi 中確認)
示例: fst 的定義
fst' :: (a, b) -> a
fst' (x, _) = x
太神奇了!我們沒有在等式的左側使用普通變數,而是用 2 元組的模式(即(,))指定了引數,該模式用一個變數和_ 模式填充。然後,該變數會自動與元組的第一個分量相關聯,我們使用它來編寫等式的右側。當然,snd 的定義類似。
此外,上面演示的技巧也可以用於列表。以下是head 和tail 的實際定義
示例: head、tail 和模式
head :: [a] -> a
head (x:_) = x
head [] = error "Prelude.head: empty list"
tail :: [a] -> [a]
tail (_:xs) = xs
tail [] = error "Prelude.tail: empty list"
與上一個示例相比,唯一重要的改變是將(,) 替換為 cons 運算子(:) 的模式。這些函式還使用空列表的模式[] 有一個等式;但是,由於空列表沒有頭或尾,所以除了使用error 列印更漂亮的錯誤訊息之外,我們什麼也做不了。
總之,模式匹配的力量來自它在訪問複雜值的部分中的使用。特別是,在Recursion 及其後的章節中,列表上的模式匹配將被廣泛使用。稍後,我們將探討這個看似神奇的特性背後發生了什麼。
為了結束本章,我們簡單說一下let 繫結(一種用於進行區域性宣告的where 子句的替代方案)。例如,考慮尋找形式為 的多項式(換句話說,是二次方程的解 - 回想一下你的初中數學課)的根。它的解由下式給出
- .
我們可以編寫以下函式來計算 的兩個值
roots a b c =
((-b + sqrt(b * b - 4 * a * c)) / (2 * a),
(-b - sqrt(b * b - 4 * a * c)) / (2 * a))
然而,在兩種情況下都寫sqrt(b * b - 4 * a * c) 項很煩人;我們可以使用區域性繫結來代替,使用where 或,如將在下面演示的那樣,使用let 宣告
roots a b c =
let sdisc = sqrt (b * b - 4 * a * c)
in ((-b + sdisc) / (2 * a),
(-b - sdisc) / (2 * a))
我們將let 關鍵字放在宣告之前,然後使用in 來表示我們正在返回到函式的“主體”。可以在單個let...in 塊內放置多個宣告 - 只要確保它們的縮排量相同,否則會出現語法錯誤。
roots a b c =
let sdisc = sqrt (b * b - 4 * a * c)
twice_a = 2 * a
in ((-b + sdisc) / twice_a,
(-b - sdisc) / twice_a)
由於縮排在 Haskell 中在語法上很重要,因此您需要小心使用的是製表符還是空格。最好的解決方案是將您的文字編輯器配置為插入兩個或四個空格以代替製表符。如果您堅持將製表符保留為不同的符號,至少要確保您的製表符始終具有相同的長度。 |
注意
在 縮排 章節中,全面解釋了縮排規則。