美文网首页
Haskell 基本语法(二)模式匹配

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

作者: rollingstarky | 来源:发表于2021-06-25 22:47 被阅读0次

    模式匹配包含一系列特定的模式,用来判断数据是否符合规则,且能够通过这些模式把符合要求的数据解构出来。
    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!

    相关文章

      网友评论

          本文标题:Haskell 基本语法(二)模式匹配

          本文链接:https://www.haomeiwen.com/subject/tggnyltx.html