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."
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容