Haskell/模式匹配
在之前的模組中,我們介紹了模式匹配,並偶爾提及它。現在我們已經對這門語言有一定的瞭解了,是時候進行深入的學習了。我們將從一個簡短的描述開始討論,並在本章中進一步展開。
在模式匹配中,我們嘗試將值匹配到模式上,如果需要,可以將變數繫結到匹配成功的結果。.
模式匹配幾乎無處不在。例如,考慮這個 map 的定義
map _ [] = []
map f (x:xs) = f x : map f xs
在表面上,每個方程式包含四個不同的模式,總共兩個。
f是一個模式,它匹配任何值,並將f變數繫結到匹配到的任何值。(x:xs)是一個模式,它匹配一個非空列表,該列表由一個值組成(該值被繫結到x變數)使用(:)函式連線到另一個值上(該值被繫結到xs)。[]是一個模式,它匹配空列表。它不繫結任何變數。_是一個模式,它匹配任何值而不繫結(萬用字元,“不關心”模式)。
在 (x:xs) 模式中,x 和 xs 可以被視為用於匹配列表各個部分的子模式。就像 f 一樣,它們匹配任何值 - 儘管很明顯,如果匹配成功且 x 的型別為 a,則 xs 的型別將為 [a]。最後,這些考慮表明 xs 也會匹配空列表,因此一個元素列表也會匹配 (x:xs)。
從上面的分析中,我們可以說模式匹配給我們提供了一種方法來
- 識別值。例如,當
map被呼叫且第二個引數匹配[]時,map的第一個方程式被使用,而不是第二個方程式。 - 將變數繫結到識別到的值。在這種情況下,當使用第二個方程式時,
f、x和xs變數被分配到傳遞給map的引數值,因此我們可以在=右側透過變數使用這些值。正如_和[]所示,繫結不是模式匹配的必要部分,而只是使用變數名作為模式的副作用。 - 將值分解成各個部分,就像
(x:xs)模式透過將兩個變數繫結到匹配引數(非空列表)的各個部分(頭部和尾部)來做的那樣。
儘管上面進行了詳細的分析,但我們似乎仍然感覺有些神奇,我們似乎分解了列表,就好像我們在撤銷 (:) 運算子的效果。小心:這個過程不適用於任何任意的運算子。例如,人們可能會想到定義一個函式,使用 (++) 來截斷列表的前三個元素。
dropThree ([x,y,z] ++ xs) = xs
但這將不起作用。(++) 函式不允許在模式中使用。事實上,大多數其他對列表進行操作的函式都被禁止進行模式匹配。那麼,哪些函式是允許的呢?
簡單地說,建構函式 - 用於構建代數資料型別值的函式。讓我們考慮一個隨機的例子
data Foo = Bar | Baz Int
這裡 Bar 和 Baz 是 Foo 型別 的建構函式。你可以將它們用於匹配 Foo 值的模式,並將變數繫結到用 Baz 構造的 Foo 中包含的 Int 值。
f :: Foo -> Int
f Bar = 1
f (Baz x) = x - 1
這與型別宣告模組中的 showAnniversary 和 showDate 完全一樣。例如
data Date = Date Int Int Int -- Year, Month, Day
showDate :: Date -> String
showDate (Date y m d) = show y ++ "-" ++ show m ++ "-" ++ show d
showDate 定義左側的 (Date y m d) 模式匹配一個 Date(用 Date 建構函式構建)並將 y、m 和 d 變數繫結到 Date 值的內容。
至於列表,就模式匹配而言,它們與用 data 定義的代數資料型別沒有區別。它的工作原理就好像列表是用這個 data 宣告定義的(注意以下語法實際上無效:列表實際上與 Haskell 結合得太緊密,無法像這樣定義)。
data [a] = [] | a : [a]
因此,空列表 [] 和 (:) 函式是列表資料型別的建構函式,因此你可以用它們進行模式匹配。[] 不接受任何引數,因此當它用於模式匹配時,不會繫結任何變數。(:) 接受兩個引數,列表頭部和尾部,因此當模式被識別時,可能會將變數繫結到它們。
Prelude> :t [] [] :: [a] Prelude> :t (:) (:) :: a -> [a] -> [a]
此外,由於 [x, y, z] 只是 x:y:z:[] 的語法糖,我們可以使用僅包含模式匹配來實現類似於 dropThree 的功能。
dropThree :: [a] -> [a]
dropThree (_:_:_:xs) = xs
dropThree _ = []
第一個模式將匹配任何至少包含三個元素的列表。當列表無法匹配主模式時,通用的第二個定義提供了一個合理的預設值[1],從而避免了由於模式匹配失敗而導致的執行時崩潰。
注意
從我們可以用簡單的模式匹配來編寫 dropThree 函式這一事實並不意味著我們應該這樣做!雖然解決方案很簡單,但為如此特定的東西編寫程式碼仍然是一種浪費。我們可以直接使用 Prelude 並用 drop 3 xs 來解決這個問題。就像之前關於直接編寫遞迴函式的說法一樣,我們也可以說:不要對模式匹配過於興奮……
類似的考慮適用於元組。我們透過模式匹配訪問它們的元件……
fstPlusSnd :: (Num a) => (a, a) -> a
fstPlusSnd (x, y) = x + y
norm3D :: (Floating a) => (a, a, a) -> a
norm3D (x, y, z) = sqrt (x^2 + y^2 + z^2)
……是透過元組建構函式的存在來實現的。對於對,建構函式是逗號運算子 (,);對於更大的元組,有 (,,);(,,,) 等等。這些運算子有點特殊,因為我們不能以常規方式使用它們作為中綴運算子;因此 5 , 3 不是編寫 (5, 3) 的有效方法。然而,所有這些都可以用字首方式使用,這在某些情況下非常有用。
Prelude> (,) 5 3
(5,3)
Prelude> (,,,) "George" "John" "Paul" "Ringo"
("George","John","Paul","Ringo")
如本書前面所述,像這樣簡單的分段函式定義
f :: Int -> Int
f 0 = 1
f 1 = 5
f 2 = 2
f _ = -1
也在進行模式匹配,將 f 的引數與 Int 字面值 0、1 和 2 進行匹配,最後與 _ 匹配。通常,數值和字元字面值可以在模式匹配中單獨使用[2],也可以與建構函式模式一起使用。例如,這個函式
g :: [Int] -> Bool
g (0:[]) = False
g (0:xs) = True
g _ = False
對於 [0] 列表將計算為 False,如果列表的第一個元素為 0 且尾部非空,則計算為 True,在所有其他情況下計算為 False。此外,包含字面元素的列表,例如 [1,2,3],甚至 "abc"(等同於 ['a','b','c'])也可以用於模式匹配,因為這些形式只是 (:) 建構函式的語法糖。
上述考慮僅適用於字面值,因此以下程式碼將不起作用
k = 1
--again, this won't work as expected
h :: Int -> Bool
h k = True
h _ = False
| 練習 |
|---|
|
語法技巧
[edit | edit source]作為模式
[edit | edit source]有時,在匹配值中的子模式時,將名稱繫結到正在匹配的整個值可能很有用。作為模式允許這樣做:它們的形式為 var@pattern,並且還具有將名稱 var 繫結到由 pattern 匹配的整個值的效果。例如,以下是對 map 主題的玩具變體
contrivedMap :: ([a] -> a -> b) -> [a] -> [b]
contrivedMap f [] = []
contrivedMap f list@(x:xs) = f list x : contrivedMap f xs
contrivedMap 不僅將 x 傳遞給引數函式 f,還傳遞了每個遞迴呼叫的引數使用的未分割列表。在沒有作為模式的情況下編寫它會有點笨拙,因為我們必須要麼使用 head,要麼不必要地重新構造list 的原始值,即實際上在右側計算 x:xs
contrivedMap :: ([a] -> a -> b) -> [a] -> [b]
contrivedMap f [] = []
contrivedMap f (x:xs) = f (x:xs) x : contrivedMap f xs
| 練習 |
|---|
實現 scanr,如 列表 III 中的練習,但這次使用作為模式。 |
記錄簡介
[edit | edit source]對於具有多個元素的建構函式,記錄 提供了一種使用以下語法在資料型別中命名值的方法
data Foo2 = Bar2 | Baz2 {bazNumber::Int, bazName::String}
使用記錄允許僅對與我們正在編寫的函式相關的變數進行匹配和繫結,從而使程式碼更清晰
h :: Foo2 -> Int
h Baz2 {bazName=name} = length name
h Bar2 {} = 0
x = Baz2 1 "Haskell" -- construct by declaration order, try ":t Baz2" in GHCi
y = Baz2 {bazName = "Curry", bazNumber = 2} -- construct by name
h x -- 7
h y -- 5
此外,即使您在 data 宣告中不使用記錄,{} 模式也可以用於匹配建構函式,而不管資料型別元素是什麼。
data Foo = Bar | Baz Int
g :: Foo -> Bool
g Bar {} = True
g Baz {} = False
如果我們修改建構函式 Bar 或 Baz 的元素數量或型別,函式 g 不必更改。
使用記錄語法還有其他優勢,我們將在 命名欄位 章的更多關於資料型別部分中詳細介紹。
我們可以在哪裡使用模式匹配
[edit | edit source]簡短的答案是,無論您可以在哪裡繫結變數,您都可以進行模式匹配。讓我們看一下我們以前見過的這些地方;在後面的章節中將介紹一些更多的地方。
方程
[edit | edit source]最明顯的用例是函式定義方程的左側,這是我們迄今為止示例的主題。
map _ [] = []
map f (x:xs) = f x : map f xs
在 map 定義中,我們對兩個方程的左側進行模式匹配,並在第二個方程上繫結變數。
let 表示式和 where 子句
[edit | edit source]let 和 where 都是進行區域性變數繫結的方法。因此,您也可以在其中使用模式匹配。一個簡單的例子
y = let (x:_) = map (*2) [1,2,3]
in x + 5
或者,等效地,
y = x + 5
where
(x:_) = map (*2) [1,2,3]
這裡,x 將繫結到 map ((*) 2) [1,2,3] 的第一個元素。因此,y 將計算為 .
Lambda 抽象
[edit | edit source]模式匹配可以直接在 lambda 抽象中使用
swap = \(x,y) -> (y,x)
然而,很明顯,此語法只允許一個模式(或者在多引數 lambda 抽象的情況下,每個引數一個模式)。
列表推導
[edit | edit source]在列表推導中的 | 之後,您可以進行模式匹配。這實際上非常有用,並且為推導的表達能力增加了許多功能。讓我們看看它是如何在一個稍微複雜一點的例子中工作的。Prelude 提供了一個 Maybe 型別,它具有以下建構函式
data Maybe a = Nothing | Just a
它通常用於儲存操作的結果,該操作可能成功也可能失敗;如果操作成功,則使用 Just 建構函式並將值傳遞給它;否則使用 Nothing。[3] 實用函式 catMaybes(可從 Data.Maybe 庫模組獲得)接受一個 Maybe 列表(可能包含“Just”和“Nothing”Maybe),並透過過濾掉 Nothing 值並刪除 Just x 的 Just 包裝器來檢索包含的值。使用列表推導編寫它非常簡單
catMaybes :: [Maybe a] -> [a]
catMaybes ms = [ x | Just x <- ms ]
使用列表推導執行此任務的另一個好處是,如果模式匹配失敗(即它遇到 Nothing),它只會繼續執行 ms 中的下一個元素,從而避免需要使用備用函式定義顯式處理我們不感興趣的建構函式。[4]
do 塊
[edit | edit source]在 簡單輸入和輸出 章中使用的 do 塊中,我們可以使用左箭頭變數繫結的左側進行模式匹配
putFirstChar = do
(c:_) <- getLine
putStrLn [c]
此外,就模式匹配而言,do 塊中的 let 繫結與“實際”的 let 表示式相同。
筆記
- ↑ 對於此特定任務合理,並且僅僅是因為期望
dropThree在應用於一個列表(例如,兩個元素)時會給出[]。對於不同的問題,如果第一次匹配失敗,返回任何列表可能並不合理。在後面的章節中,我們將考慮一種處理此類情況的簡單方法。 - ↑ 正如可能預期的那樣,這種使用字面量的匹配不是基於建構函式的。相反,幕後有一個相等性比較
- ↑ 這種操作的典型例子是在字典中查詢值 - 這可能只是一個
[(a, b)]列表,其中元組是鍵值對,或者是一個更復雜的實現。無論如何,如果我們給定一個任意鍵,嘗試檢索一個值,我們無法保證我們實際上會找到與該鍵關聯的值。 - ↑ 它以這種方式工作而不是在模式匹配失敗時崩潰的原因與列表推導的真實本質有關:它們實際上是列表單子的包裝器。當我們討論單子時,我們最終會解釋這意味著什麼。