模式匹配
模式匹配本质上是一种分支选择功能,可以在定义函数的时候就进行分支选择,而不是在函数内部使用 if ... else
语句,使代码逻辑更加清晰
下面的例子第一个模式匹配数字 7
,最后一个模式应该是一个万能匹配
注意:类似的模式,优先级高的模式应该放到前面
lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"
下面的例子,匹配 数字 1
到 5
,省略很多 if
else
语句
sayMe :: (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"
下面是个标准的递归函数,如果不用模式匹配,则退出条件的判断需要用到 if
else
语句
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)
模式匹配如果失败了,即没有找到符合条件的模式,则会抛出错误
charName :: Char -> String
charName 'a' = "Albert"
charName 'b' = "Broseph"
charName 'c' = "Cecil"
ghci> charName 'a'
"Albert"
ghci> charName 'b'
"Broseph"
ghci> charName 'h'
"*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName
因此一定要保留一个万能模式,可以匹配所有情况
解构
所谓解构,就是利用数据的构成模式,获取数据中的组成部分,省略了拆解和封装的过程
数据的构成模式既然也是一种模式,自然可以应用到模式匹配中去。
元组的模式匹配
处理元组时,充分利用解构和模式匹配,可以省去从元组中取出数据的步骤。
例如,两个序对相加,传统方法需要用 fst
和 snd
函数从序对中先取出数据,相加以后在组合。如下:
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)
使用解构和模式匹配就非常简单,因为数据传入的时候已经被分解了,函数内部不再需要拆解数据。
addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)
注意:解构模式必须放到圆括号中,每一对圆括号解构一个参数
系统并没有提供从三元组中取出数据的的函数,我们可以利用模式匹配自定义相关函数,非常简单明了
first :: (a, b, c) -> a
first (x, _, _) = x
second :: (a, b, c) -> b
second (_, y, _) = y
third :: (a, b, c) -> c
third (_, _, z) = z
下划线表示,解构出来的这一部分没什么用,忽略这一部分
列表的模式匹配
前面讲过,列表有两种构成方式
- 用
:
构建列表 - 用方括号
[]
构建字面量列表
冒号构建列表,即 x:xs
这模式的应用非常广泛,尤其是递归函数。缺点是它只能匹配长度大于等于 1 的 List。
注意:用方括号解构列表,解构模式必须和数据的长度一样,比如
[x,y,_,z] -> [1,2,3,4]
下面我们实现一个自定义的 head
函数
head' :: [a] -> a
head' [] = error "Can't call head on an empty list, dummy!"
head' (x:_) = x
注意:这里对于空列表用
error
函数抛出了一个错误,一般不会这么做,会导致程序退出
下面用模式匹配实现一个 length
函数
length' :: (Num b) => [a] -> b
length` [] = 0
length` [_, xs] = 1 + length` xs
再用模式匹配实现一个自定义的 sum
函数
sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs
用 as
模式匹配整体
解构以后,数据被拆分为很多部分,如果要使用整体数据,还要将数据再组合起来,这是重复工作。
可以在解构的时候用 @
指定一个名字,引用整体数据
下面的代码打印整个句子和句子的第一个字母
capital :: String -> String
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]
警戒 Guards
前面讲过,模式匹配已经是一种分支判断结构了,但是模式匹配只能判断和模式相等的情况,其他情况就无能为力了。
而警戒可以:在参数匹配到某种模式之后,在正式使用之前,进一步进行分支。因此,
- 警戒用在模式匹配之后,等号
=
之前 - 警戒部分是个逻辑判断表达式
- 警戒比
if ... else ...
更具可读性。
下面的代码根据某些指标计算是否超重
bmiTell :: (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!"
注意:警戒的竖线必须用空格缩进,不能用
Tab
缩进
警戒中使用表达式
警戒中可以使用任何表达式,只要最终能返回真假值即可,就像 if
语句中逻辑条件可以使用表达式一样
下面的例子,在警戒中用表达式进行了运算,先使用身高和体重计算指标,然后根据指标进行分支判断
bmiTell :: (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!"
下面实现一个自定义的 max
函数,在警戒中仅仅是个比较大小,没有进行其他运算,和 if
else
结构非常像,但是更具可读性
max' :: (Ord a) => a -> a -> a
max' a b
| a > b = a
| otherwise = b
下面实现实现一个 compaer
函数,同样在警戒中使用了比较函数
myCompare :: (Ord a) => a -> a -> Ordering
a `myCompare` b
| a > b = GT
| a == b = EQ
| otherwise = LT
Where 关键字
前面代码中,我们在每个警戒中计算身体指标,然后再进行逻辑判断。问题是,计算指标的公式都是一样的,每个守卫计算了一次,出现了重复。
用 where
定义变量
用 where
可以为函数的一个模式定义局部变量,除去重复
bmiTell :: (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
注意:
where
也需要缩进,和竖线同样缩进即可
最好给字面量也定义名字,代码更具可读性。注意:这里给常量赋值使用了模式匹配
bmiTell :: (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, normal, fat) = (18.5, 25.0, 30.0)
用 where
定义函数
在 where
中定义函数也是可以的,下面用列表解析计算一系列的身体指标数据
注意:这里没有用到警戒,
where
中定义的变量和函数对整个模式都是有效的
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
where bmi weight height = weight / height ^ 2
Let ... In 表达式
let ... in
结构同样是给局部数据定义名字,但是和 where
又有所不同
-
let ... in
结构是表达式,有返回值 -
let
中定义的名字只能在in
中使用,其他地方看不见 -
let
中同样可以使用模式匹配,定义函数
下面看一个使用 let
的例子,定义了两个中间值的名字,让最后返回值得表达式更加清晰简练
cylinder :: (RealFloat a) => a -> a -> a
cylinder r h =
let sideArea = 2 * pi * r * h
topArea = pi * r ^2
in sideArea + 2 * topArea
let
是个表达式
因为 let
是个表达式,本质上就是个值,所以可以放到任何地方,比如
ghci> 4 * (let a = 9 in a + 1) + 2
42
在 let
中定义函数
ghci> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]
在一行中绑定多个名字
如果要在一行中绑定多个名字,则用分号将他们分开;
如果在一行内有多个 let
则用逗号分开
ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)
(6000000,"Hey there!")
在 let
中使用模式匹配绑定名字
ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100
600
在列表解析中使用 let`
可以把 let
应用到列表解析中,计算中间值,似乎比使用 where
更简洁
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]
配合过滤语句,将胖子过滤出来
calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]
省略 in
你可能注意到了,在列表解析中使用 let
可以省略 in
语句,如果非要使用 in
,则定义的名字只能在 in
中使用。
在交互式坏境中使用 let
也可以省略 in
语句,则定义的名字在整个交互中可见。
ghci> let zoot x y z = x * y + z
ghci> zoot 3 9 2
29
ghci> let boot x y z = x * y + z in boot 3 4 2
14
ghci> boot
< interactive>:1:0: Not in scope: `boot'
case ... of
表达式
case ... of
表达式就是模式匹配,但是因为他是表达式可以用在任何地方,而模式匹配只能在定义参数的时候使用
describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of [] -> "empty."
[x] -> "a singleton list."
xs -> "a longer list."
注意:
case ... of
表达式中,模式后面不使用等号,而是使用箭头符号
如果用模式匹配则是这样
describeList :: [a] -> String
describeList xs = "The list is " ++ what xs
where what [] = "empty."
what [x] = "a singleton list."
what xs = "a longer list."