式匹配包含一系列特定的模式,用来判断数据是否符合规则,且能够通过这些模式把符合要求的数据解构出来。
Haskell 中的模式匹配可以应用到任意的数据类型上(数字、字符、列表、元组等等)。
函数中的模式匹配
可以在函数体的定义中,用不同的代码行分别指定不同的模式:1
2
3
4
5
6
7
8
9
10
11
12Prelude> :{
Prelude| lucky :: (Integral a) => a -> String
Prelude| lucky 7 = "LUCKY NUMBER SEVEN!"
Prelude| lucky x = "Sorry, you're out of luck!"
Prelude| :}
Prelude>
Prelude> lucky 1
"Sorry, you're out of luck!"
Prelude> lucky 10
"Sorry, you're out of luck!"
Prelude> lucky 7
"LUCKY NUMBER SEVEN!"
PS:上述代码是在 Haskell 的交互式解释器(REPL) ghci
中定义和执行函数的效果。
像 lucky
这种包含多行代码的函数,在 ghci 解释器中直接定义时,需要把整个函数体用 :{
和 :}
括起来(否则解释器会报错)。实际的 lucky
函数代码应为:1
2
3lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
即 :{
和 :}
从代码的角度讲是多余的,只是 ghci
解释器的缘故,导致必须加上这两个分隔符。若在文件中编写代码,则应该使用第二种形式。
代码的第一行 lucky :: (Integral a) => a -> String
是函数的类型签名,也可以省略,解释器会自行推导。lucky 7
和 lucky x
两行代码则指定了具体的两个模式:
当函数输入为数字 7 时匹配第一个模式,任何其他的数字输入则匹配第二个模式并将该输入值绑定给变量 x
。
一个包含更多个模式的函数:1
2
3
4
5
6
7sayMe :: (Integral a) => a -> String
sayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
sayMe 4 = "Four!"
sayMe 5 = "Five!"
sayMe x = "Not between 1 and 5"
需要注意的是,最后一行代码 sayMe x
必须作为最后一个模式。
函数体中的模式会按照自顶而下的顺序检查是否匹配,若当前的模式已完成匹配,则忽略后面的检查;若当前模式不匹配,则继续向下逐个进行检查。
若 sayMe x
作为顶部的第一个模式(它实际上会匹配所有合法值),则任何输入值都会在第一步就完成匹配,进而忽略后面的 sayMe 1
、sayMe 2
等模式,不再进行判断。即输入任何数字都会先匹配 x
并输出 Not between 1 and 5。
使用模式匹配和递归实现阶乘函数1
2
3factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
比如当输入为 3 时,factorial
函数会匹配第二个模式,结果为 3 * (factorial 2)
。继续迭代,进一步计算结果中的 factorial 2
,得到 3 * (2 * (factorial 1))
、3 * (2 * (1 * (factorial 0)))
。
而 factorial 0
会匹配第一个模式得到结果 1,迭代终止,再和前面的数字相乘后得到最终结果。
假如将 factorial n = n * factorial (n - 1)
作为第一个模式,则 factorial n
会匹配包含数字 0 在内的所有数字,另一个模式 factorial 0 = 1
就永远不会触发。从而导致迭代没有终止条件,一直进行下去。
因此,在模式匹配中,更精确更有指向性的模式总是放在相对通用和宽泛的模式前面。
在使用模式匹配时,应该总是包含一个 catch-all 模式,这样就不会出现所有模式都不匹配的情况。若程序的输入与所有模式都不匹配,程序会崩溃掉。1
2
3
4
5
6
7
8
9
10
11
12
13Prelude> :{
Prelude| charName :: Char -> String
Prelude| charName 'a' = "Albert"
Prelude| charName 'b' = "Broseph"
Prelude| charName 'c' = "Cecil"
Prelude| :}
Prelude>
Prelude> charName 'a'
"Albert"
Prelude> charName 'b'
"Broseph"
Prelude> charName 'h'
*** Exception: <interactive>:(3,1)-(5,22): Non-exhaustive patterns in function charName
元组中的模式匹配
在不使用模式匹配的情况下,实现一个计算两个向量之和的函数:1
2addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)
通过模式匹配实现上述功能:1
2
3
4
5
6
7Prelude> :{
Prelude| addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
Prelude| addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
Prelude| :}
Prelude>
Prelude> addVectors (1, 2) (3, 4)
(4,6)
fst
和 snd
函数可以分别用来获取元组中的第一个和第二个元素(但是只针对包含两个元素的元组)。
对于有 3 个元素的元组,实际上可以借助模式匹配自己实现:1
2
3
4
5
6
7
8first :: (a, b, c) -> a
first (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z
可以在列表推导中使用模式匹配:1
2
3Prelude> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
Prelude> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]
甚至列表本身也可以用于模式匹配。
如模式 x:xs
会将列表的第一个元素绑定给变量 x
,把其余元素绑定给 xs
。此模式的应用非常普遍,尤其是在递归函数中。
如果想提取列表的前 3 个元素并将它们绑定给指定变量,可以使用 x:y:z:zs
形式的模式。
利用对列表的模式匹配实现自定义的 head
函数(获取列表中的第一个元素):1
2
3
4
5
6
7
8
9
10Prelude> :{
Prelude| head' :: [a] -> a
Prelude| head' [] = error "Can't call head on an empty list!"
Prelude| head' (x:_) = x
Prelude| :}
Prelude>
Prelude> head' [4, 5, 6]
4
Prelude> head' "Hello"
'H'
借助递归和模式匹配实现自定义的 length
函数(获取列表的长度):1
2
3length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs
对于任何一个合法的输入如 "ham"
,length'
函数的计算过程如下:length' "ham"
=> 1 + length' "am"
=> 1 + (1 + length' "m")
=> 1 + (1 + (1 + length' []))
=> 1 + (1 + (1 + 0))
实现自定义的 sum
函数(求列表中各元素之和):1
2
3sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
守卫(guards)
守卫一般用来测试某个(些)值的特定属性是否为真,很像 if 语句。守卫和模式整合得非常好。
以下是一个求 BMI(体重指数)的函数定义:1
2
3
4
5
6bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
管道符(|
)后面的布尔表达式即为守卫的定义。若该表达式计算结果为 True,则对应的代码被执行。
若该表达式计算结果为 False,则继续测试下一个守卫。
通常情况下,最后一个守卫是 otherwise
。它其实是 otherwise = True
的简写形式,会捕获所有剩余的情况。
守卫可以配合有多个参数的函数使用:1
2
3
4
5
6bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
1 | Prelude> bmiTell 65 1.75 |
通过守卫自定义 max
函数:1
2
3
4max' :: (Ord a) => a -> a -> a
max' a b
| a > b = a
| otherwise = b
where
可以通过 where
关键字优化上面的 bmiTell
函数:1
2
3
4
5
6
7bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= 18.5 = "You're underweight, you emo, you!"
| bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
变量 bmi
在这里只计算了一次,不同于之前的 weight / height ^ 2
有可能会被重复计算 3 次。
更进一步,bmiTell
函数还可以改为如下形式:1
2
3
4
5
6
7
8
9
10bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| bmi <= skinny = "You're underweight, you emo, you!"
| bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
| bmi <= fat = "You're fat! Lose some weight, fatty!"
| otherwise = "You're a whale, congratulations!"
where bmi = weight / height ^ 2
skinny = 18.5
normal = 25.0
fat = 30.0
where
语句中也可以定义函数,比如通过由多个包含身高体重的元组组成的列表,计算一系列 BMI 值:1
2
3calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
where bmi weight height = weight / height ^ 2