04 函数的语法

模式匹配

模式匹配本质上是一种分支选择功能,可以在定义函数的时候就进行分支选择,而不是在函数内部使用 if ... else 语句,使代码逻辑更加清晰

下面的例子第一个模式匹配数字 7 ,最后一个模式应该是一个万能匹配

注意:类似的模式,优先级高的模式应该放到前面

lucky :: (Integral a) => a -> String  
lucky 7 = "LUCKY NUMBER SEVEN!"  
lucky x = "Sorry, you're out of luck, pal!"

下面的例子,匹配 数字 15,省略很多 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

因此一定要保留一个万能模式,可以匹配所有情况

解构

所谓解构,就是利用数据的构成模式,获取数据中的组成部分,省略了拆解和封装的过程

数据的构成模式既然也是一种模式,自然可以应用到模式匹配中去。

元组的模式匹配

处理元组时,充分利用解构和模式匹配,可以省去从元组中取出数据的步骤。

例如,两个序对相加,传统方法需要用 fstsnd 函数从序对中先取出数据,相加以后在组合。如下:

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

你可能感兴趣的:(04 函数的语法)