6

Haskell 基本语法(二)模式匹配

 3 years ago
source link: https://www.starky.ltd/2021/06/25/basic-haskell-pattern-match/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Haskell 基本语法(二)模式匹配

发表于 2021-06-25

| 分类于 Program

|

| 阅读次数:

字数统计: 6.2k

|

阅读时长 ≈ 0:06

式匹配包含一系列特定的模式,用来判断数据是否符合规则,且能够通过这些模式把符合要求的数据解构出来。
Haskell 中的模式匹配可以应用到任意的数据类型上(数字、字符、列表、元组等等)。

函数中的模式匹配

可以在函数体的定义中,用不同的代码行分别指定不同的模式:

Prelude> :{
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 函数代码应为:

lucky :: (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 7lucky x 两行代码则指定了具体的两个模式:
当函数输入为数字 7 时匹配第一个模式,任何其他的数字输入则匹配第二个模式并将该输入值绑定给变量 x

一个包含更多个模式的函数:

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"

需要注意的是,最后一行代码 sayMe x 必须作为最后一个模式。
函数体中的模式会按照自顶而下的顺序检查是否匹配,若当前的模式已完成匹配,则忽略后面的检查;若当前模式不匹配,则继续向下逐个进行检查。
sayMe x 作为顶部的第一个模式(它实际上会匹配所有合法值),则任何输入值都会在第一步就完成匹配,进而忽略后面的 sayMe 1sayMe 2 等模式,不再进行判断。即输入任何数字都会先匹配 x 并输出 Not between 1 and 5。

使用模式匹配和递归实现阶乘函数

factorial :: (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 模式,这样就不会出现所有模式都不匹配的情况。若程序的输入与所有模式都不匹配,程序会崩溃掉。

Prelude> :{
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

元组中的模式匹配

在不使用模式匹配的情况下,实现一个计算两个向量之和的函数:

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)

通过模式匹配实现上述功能:

Prelude> :{
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)

fstsnd 函数可以分别用来获取元组中的第一个和第二个元素(但是只针对包含两个元素的元组)。
对于有 3 个元素的元组,实际上可以借助模式匹配自己实现:

first :: (a, b, c) -> a
first (x, _, _) = x

second :: (a, b, c) -> b
second (_, y, _) = y

third :: (a, b, c) -> c
third (_, _, z) = z

可以在列表推导中使用模式匹配:

Prelude> 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 函数(获取列表中的第一个元素):

Prelude> :{
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 函数(获取列表的长度):

length' :: (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 函数(求列表中各元素之和):

sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs

守卫(guards)

守卫一般用来测试某个(些)值的特定属性是否为真,很像 if 语句。守卫和模式整合得非常好。

以下是一个求 BMI(体重指数)的函数定义:

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!"

管道符(|)后面的布尔表达式即为守卫的定义。若该表达式计算结果为 True,则对应的代码被执行。
若该表达式计算结果为 False,则继续测试下一个守卫。

通常情况下,最后一个守卫是 otherwise。它其实是 otherwise = True 的简写形式,会捕获所有剩余的情况。

守卫可以配合有多个参数的函数使用:

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!"


Prelude> bmiTell 65 1.75
"You're supposedly normal. Pffft, I bet you're ugly!"

通过守卫自定义 max 函数:

max' :: (Ord a) => a -> a -> a
max' a b
| a > b = a
| otherwise = b

where
可以通过 where 关键字优化上面的 bmiTell 函数:

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

变量 bmi 在这里只计算了一次,不同于之前的 weight / height ^ 2 有可能会被重复计算 3 次。

更进一步,bmiTell 函数还可以改为如下形式:

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 = 18.5
normal = 25.0
fat = 30.0

where 语句中也可以定义函数,比如通过由多个包含身高体重的元组组成的列表,计算一系列 BMI 值:

calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
where bmi weight height = weight / height ^ 2

Learn You a Haskell for Great Good!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK