Haskell趣学指南4-6

 

函数的语法

模式匹配

本章讲的就是haskell那套酷酷的语法结构,先从模式匹配开始。模式匹配通过检查数据的特定结构来检查其是否匹配,并按模式从中取得数据。

在定义函数时,你可以为不同的模式分别定义函数体,这就让代码更加简洁易读。你可以匹配一切数据类型---数字,字符,List,元组,等等。我们弄个简单函数,让它检查我们传给它的数字是不是7。

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

在调用lucky时,模式会从上至下进行检查,一旦有匹配,那对应的函数体就被应用了。这个模式中的唯一匹配是参数为7,如果不是7,就转到下一个模式,它匹配一切数值并将其绑定为x。这个函数完全可以使用if实现,不过我们若要个分辨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"  

注意下,如果我们把最后匹配一切的那个模式挪到最前,它的结果就全都是"Not between 1 and 5"  了。因为它自己匹配了一切数字,不给后面的模式留机会。

记得前面实现的那个阶乘函数么?当时是把n的阶乘定义成了product [1..n]。也可以写出像数学那样的递归实现,先说明0的阶乘是1,再说明每个正整数的阶乘都是这个数与它前驱(predecessor)对应的阶乘的积。如下便是翻译到haskell的样子:

factorial :: (Integral a) => a -> a   
factorial 0 = 1   
factorial n = n * factorial (n - 1)  

这就是我们定义的第一个递归函数。递归在haskell中十分重要,我们会在后面深入理解。如果拿一个数(如3)调用factorial函数,这就是接下来的计算步骤:先计算3*factorial 2factorial 2等于2*factorial 1,也就是3*(2*(factorial 1))factorial 1等于1*factorial 0,好,得3*(2*(1*factorial 0)),递归在这里到头了,嗯---我们在万能匹配前面有定义,0的阶乘是1.于是最终的结果等于3*(2*(1*1))。若是把第二个模式放在前面,它就会捕获包括0在内的一切数字,这一来我们的计算就永远都不会停止了。这便是为什么说模式的顺序是如此重要:它总是优先匹配最符合的那个,最后才是那个万能的。

模式匹配也会失败。假如这个函数:

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  

它告诉我们说,这个模式不够全面。因此,在定义模式时,一定要留一个万能匹配的模式,这样我们的程序就不会为了不可预料的输入而崩溃了。

对Tuple同样可以使用模式匹配。写个函数,将二维空间中的向量相加该如何?将它们的x项和y项分别相加就是了。如果不了解模式匹配,我们很可能会写出这样的代码:

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)  

there we go!好多了!注意,它已经是个万能的匹配了。两个addVector的类型都是addVectors:: (Num a) => (a,a) -> (a,a) -> (a,a),我们就能够保证,两个参数都是序对(Pair)了。

fst和snd可以从序对中取出元素。三元组(Tripple)呢?嗯,没现成的函数,得自己动手:

first :: (a, b, c) -> a   
first (x, _, _) = x   
 
second :: (a, b, c) -> b   
second (_, y, _) = y   
  
third :: (a, b, c) -> c   
third (_, _, z) = z  

这里的_就和List Comprehension中一样。表示我们不关心这部分的具体内容。

说到List Comprehension,我想起来在List Comprehension中也能用模式匹配:

ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]   
ghci> [a+b | (a,b) <- xs]   
[4,7,6,8,11,4]

一旦模式匹配失败,它就简单挪到下个元素。

对list本身也可以使用模式匹配。你可以用[]:来匹配它。因为[1,2,3]本质就是1:2:3:[]的语法糖。你也可以使用前一种形式,像x:xs这样的模式可以将list的头部绑定为x,尾部绑定为xs。如果这list只有一个元素,那么xs就是一个空list。

Note:x:xs这模式的应用非常广泛,尤其是递归函数。不过它只能匹配长度大于等于1的list。

如果你要把list的前三个元素都绑定到变量中,可以使用类似x:y:z:xs这样的形式。它只能匹配长度大于等于3的list。

我们已经知道了对list做模式匹配的方法,就实现个我们自己的head函数。

head' :: [a] -> a   
head' [] = error "Can't call head on an empty list, dummy!"   
head' (x:_) = x  

看看管不管用:

ghci> head' [4,5,6]   
4   
ghci> head' "Hello"   
'H'  

漂亮!注意下,你若要绑定多个变量(用_也是如此),我们必须用括号将其括起。同时注意下我们用的这个error函数,它可以生成一个运行时错误,用参数中的字符串表示对错误的描述。它会直接导致程序崩溃,因此应谨慎使用。可是对一个空list取head真的不靠谱哇。

弄个简单函数,让它用非标准的英语给我们展示list的前几项。

tell :: (Show a) => [a] -> String   
tell [] = "The list is empty"   
tell (x:[]) = "The list has one element: " ++ show x   
tell (x:y:[]) = "The list has two elements: " ++ show x ++ " and " ++ show y   
tell (x:y:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y  

这个函数顾及了空list,单元素list,双元素list以及较长的list,所以这个函数很安全。(x:[])(x:y:[])也可以写作[x][x,y](有了语法糖,我们不必多加括号)。不过(x:y:_)这样的模式就不行了,因为它匹配的list长度不固定。

我们曾用List Comprehension实现过自己的length函数,现在用模式匹配和递归重新实现它:

length' :: (Num b) => [a] -> b   
length' [] = 0   
length' (_:xs) = 1 + length' xs  

这与先前写的那个factorial函数很相似。先定义好未知输入的结果---空list,这也叫作边界条件。再在第二个模式中将这List分割为头部和尾部。说,List的长度就是其尾部的长度加1。匹配头部用的_,因为我们并不关心它的值。同时也应明确,我们顾及了List所有可能的模式:第一个模式匹配空list,第二个匹配任意的非空list。

看下拿"ham"调用length'会怎样。首先它会检查它是否为空List。显然不是,于是进入下一模式。它匹配了第二个模式,把它分割为头部和尾部并无视掉头部的值,得长度就是1+length' "am"。ok。以此类推,"am"length就是1+length' "m"。好,现在我们有了1+(1+length' "m")length' "m"1+length ""(也就是1+length' [])。根据定义,length' []等于0。最后得1+(1+(1+0))

再实现sum。我们知道空list的和是0,就把它定义为一个模式。我们也知道一个list的和就是头部加上尾部的和的和。写下来就成了:

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

还有个东西叫做as模式,就是将一个名字和@置于模式前,可以在按模式分割什么东西时仍保留对其整体的引用。如这个模式xs@(x:y:ys),它会匹配出与x:y:ys对应的东西,同时你也可以方便地通过xs得到整个list,而不必在函数体中重复x:y:ys。看下这个quick and dirty的例子:

capital :: String -> String   
capital "" = "Empty string, whoops!"   
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]  
ghci> capital "Dracula"   
"The first letter of Dracula is D"  

我们使用as模式通常就是为了在较大的模式中保留对整体的引用,从而减少重复性的工作。

还有——你不可以在模式匹配中使用++。若有个模式是(xs++ys),那么这个List该从什么地方分开呢?不靠谱吧。而(xs++[x,y,z])或只一个(xs++[x])或许还能说的过去,不过出于list的本质,这样写也是不可以的。

注意,门卫!

模式用来检查一个值是否合适并从中取值,而门卫(guard)则用来检查一个值的某项属性是否为真。咋一听有点像是if语句,实际上也正是如此。不过处理多个条件分支时门卫的可读性要高些,并且与模式匹配契合的很好。

在讲解它的语法前,我们先看一个用到门卫的函数。它会依据你的BMI值(body mass index,身体质量指数)来不同程度地侮辱你。BMI值即为体重除以身高的平方。如果小于18.5,就是太瘦;如果在18.5到25之间,就是正常;25到30之间,超重;如果超过30,肥胖。这就是那个函数(我们目前暂不为您计算bmi,它只是直接取一个emi值)。

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

门卫由跟在函数名及参数后面的竖线标志,通常他们都是靠右一个缩进排成一列。一个门卫就是一个布尔表达式,如果为真,就使用其对应的函数体。如果为假,就送去见下一个门卫,如之继续。如果我们用24.3调用这个函数,它就会先检查它是否小于等于18.5,显然不是,于是见下一个门卫。24.3小于25.0,因此通过了第二个门卫的检查,就返回第二个字符串。

在这里则是相当的简洁,不过不难想象这在命令式语言中又会是怎样的一棵if-else树。由于if-else的大树比较杂乱,若是出现问题会很难发现,门卫对此则十分清楚。

最后的那个门卫往往都是otherwise,它的定义就是简单一个otherwise = True,捕获一切。这与模式很相像,只是模式检查的是匹配,而它们检查的是布尔表达式 。如果一个函数的所有门卫都没有通过(而且没有提供otherwise作万能匹配),就转入下一模式。这便是门卫与模式契合的地方。如果始终没有找到合适的门卫或模式,就会发生一个错误。

当然,门卫可以在含有任意数量参数的函数中使用。省得用户在使用这函数之前每次都自己计算bmi。我们修改下这个函数,让它取身高体重为我们计算。

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

看看我胖不胖......

ghci> bmiTell 85 1.90   
"You're supposedly normal. Pffft, I bet you're ugly!"  

Yay!我不胖!不过haskell依然说我很猥琐...什么道理...

注意下,函数名和参数的后面并没有=。许多新人容易搞出语法错误,就是因为在后面加上了=。

另一个简单的例子:实现个自己的max函数。应该还记得,它是取两个可比较的值,返回较大的那个。

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

门卫也可以堆一行里面。这样的可读性会差些,因而是不被鼓励的。即使是较短的函数也是如此,仅仅出于演示,我们可以这样重写max':

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

Ugh!一点都不好读!继续进发,用门卫实现我们自己的compare函数:

myCompare :: (Ord a) => a -> a -> Ordering   
`myCompare` b   
    | a > b     = GT   
    | a == b    = EQ   
    | otherwise = LT  
ghci> 3 `myCompare` 2   
GT  

Note:通过反单引号,我们不仅可以以中缀形式调用函数,也可以在定义函数的时候使用它。有时这样会更易读。

Where?

前一节中我们写了这个bmi计算函数:

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

注意,我们重复了3次。我们重复了3次。程序员的字典里不应该有“重复”这个词。既然发现有重复,那么给它一个名字来代替这三个表达式会更好些。嗯,我们可以这样修改:

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 = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值