Learn Haskell(五)
这一部分主要讲Haskell的函数语法。
1.模式匹配(Pattern Match)
模式匹配主要用来定义一些数据必须遵循的规则,根据他们来解析数据。在定义函数的时候,可以为不同的模式定义不同的函数体,以便写出可读性较高的代码。Haskell允许对很多种类型进行模式匹配,数值型、字符、列表、元组等等。下面是一个函数用来检查输入参数是不是7:
lucky::Int->String lucky 7 = "LUCKY NUMBER SEVEN" lucky x = "Sorry, you are out of lucky, pal!"
我们试着调用一下上面的函数:
*Main> lucky 7 "LUCKY NUMBER SEVEN" *Main> lucky 8 "Sorry, you are out of lucky, pal!"
当我们传递参数时,只有当参数等于7的时候,上面那个函数体才会执行,其他所有情况都只会执行下面的函数体。当使用小写字母开头的name(name我们在之前第一篇博客中提到过,name类似于Java中的变量,但不是变量)作为模式的话,那么这个模式属于全匹配(catchall)模式。也就是说,任何一个值又能和这个模式相匹配。并且我们可以通过这个name来引用传递进来的参数值。
上面的功能很容易通过if-else语句来实现,但是一旦需要匹配的模式很多,那么这个if-else链会变得超级长,很明显会降低代码的可读性,例如:
sayMe::Int->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语句来实现可读性会明显下降。假设我们将上面例子中的最后一个模式作为第一个,那么无论你输入什么参数,都只会打印输出Not between 1 and 5!谁叫他是全匹配模式呢?无论输入什么值,他都能匹配上,程序当然就没办法往下执行。
我们之前提到过用product[1..n]函数计算数的阶乘(!)。我们也可以使用模式匹配来重新实现:
factorial::Int->Int factorial 0 = 1 factorial n = n * (factorial (n-1))
这种方式的实现几乎和数学上的递归定义没什么两样,所以非常一目了然。这是一个递归调用的例子,下一篇博客会专门讲Hashell中的递归。
模式匹配有时候会失败。一种常见的例子如下:
charName::Char->String charName 'a' = "Albert" charname 'b' = "Broseph" charName 'c' = "Cecil"
当我们输入字符h的时候,GHCi会报错:
*Main> charName 'h' "*** Exception: 01.hs:18:1-23: Non-exhaustive patterns in function charName *Main> charName 'a' "Albert"
我们的函数中根本没定义能够匹配字符h的模式,当输入h时,程序不知道该怎么处理,自然会报错。这个例子说明,使用模式匹配的时候,必须定义一个catchall模式,这样才能应对可能出现的奇奇怪怪的输入。其实Haskell中的模式匹配和Java中的switch-case语句很像,每一个模式就是一个case,最后还别忘了定义一个default。但是模式匹配的功能肯定比Java的switch-case语句强大。Java中,switch语句只能对int类型使用,由于byte、char、short可以自动类型转换成int,也勉强可以使用。但是Haskell中的模式匹配对元组、列表、List Comprehension都有效。我们继续往下看:
2.元组与模式匹配
假设我们使用一个pair来对应二维空间的一个向量,如何对这两个向量进行加法运算(两个坐标分别想加即可),在没有学习模式匹配之前,我们可以这样实现这个功能:
addVectors :: (Double,Double)->(Double,Double)->(Double,Double) addVectors a b = (fst a + fst b, snd a + snd b)
这样可以实现,但是眨眼看去不知道a、b是什么,我们可以使用模式匹配改写一下:
addVectors' :: (Double,Double)->(Double,Double)->(Double,Double) addVectors' (x1,y1) (x2,y2) = (x1 + x2, y1 + y2)
这个改写的函数addVectors'中,我们使用(x1,y1)和(x2,y2)来匹配两个元组,只不过这本身就是catchall模式,这样改写就很明确了。对于pair来说,Haskell提供了fst和snd来分别取第一个或第二个部分的值。但对于triple来说是没有这样的内置函数的,我们可以使用模式匹配来实现我们自己的版本
first::(a,b,c)->a first (x,_,_) = x second::(a,b,c)->b second (_,y,_) = y third::(a,b,c)->c third (_,_,z) = z
这里的_有点类似于一个占位符,表示我们不关心的某个值而已。
3.List、List Comprehension和模式匹配
我们可以在List Comprehension中使用模式匹配,实际上之前我们已经使用过了:
let xs = [(1,3),(4,3),(2,4),(5,6),(5,3),(3,1)] [a + b | (a,b)<-xs]
常规的List也是可以使用模式匹配的。我们可以匹配空List[]或者是任何涉及:和[]的模式。(实际上[1,2,3]仅仅是1:2:3:[]的语法糖而已)。x:xs这样的模式将List的首元素绑定给x,而剩下的那个List绑定到xs上。若这个List只有一个元素,那么xs就是空List。包含:的模式只能匹配一个或一个以上元素的List。我们实现来一个我们自己版本的head函数来看看怎么对List使用模式匹配:
head' :: [a]->a
head' [] = error "Can't call head on empty list!"
head' (x:_) = x
需要特别说明的是,如果需要绑定多个变量,则必须使用()括起来。就像上面例子中最后一行。我们再看一个稍微复杂一些的例子:
tell::(Show a) => [a]->String tell [] = "Empty String!" tell (x:[]) = "The list has only one element: " ++ show x tell (x:y:[]) = "The list has two elements. First: " ++ show x ++ " Second is: " ++ show y tell (x:y:_) = "The list has more than two elements."
这个函数接收一个元素为Show类型的List作为参数,然后输出一个字符串。还记得Show Type Class和show函数吗?不记得看上一篇文章。最后关于List使用模式匹配需要强调一点:List模式匹配不能使用++运算符。
4.As-Pattern
还有一种特殊的模式叫做As-Pattern。As-Pattern允许我们将一个item根据模式拆成多个部分并且保持对原来这个item的引用。使用的语法是在常规的模式之前加上一个name和一个@。比如"xs@[x:y:ys]"这个模式和[x:y:ys]匹配的是一个东西,但是你可以通过xs获得这个List的引用,下面我们写一个例子:
fstLetter::String->String fstLetter ""="Empty String!" fstLetter all@(x:_)="The first letter of " ++ all ++ " is " ++ [x]
5.Guards
我们使用模式来检查输入函数的数据是否遵循某种规则,我们使用Guards来检查输入数据的某方面属性是真还是假。听起来很像if语句,实际上却是很像。但是Guards在处理多种情况时更具有可读性,并且Guards能和模式一起使用。
bmiTell :: Double->String bmiTell bmi | bmi <= 18.5 = "You are underweight!" | bmi <= 25.0 = "You are normal!" | bmi <= 30.0 = "You are fat!" | otherwise = "You are whale!"
我们不用管bmi是什么,反正这个程序接收一个Double参数,判断这个参数属于哪个范围,不同的范围输出不同的语句。和if-else语句链做的事情几乎没什么区别,只是每一个范围都只需要写上限,else用otherwise代替。
Guards的语法如下:每一个Guard由一个管道符号|所标识,后跟一个Boolean表达式,后面跟着当这个表达式为True时执行的函数体。一般来说,最后一个Guard是otherwise,这个Guard匹配所有的情况,目的和catchall模式一样。在接收多个参数的函数中使用Guards也是允许的,我们让上面的函数接收两个参数,然后改写一下:
bmiTell' :: Double->Double->String bmiTell' weight height | weight / height ^ 2 <= 18.5 = "You are underweight!" | weight / height ^ 2 <= 25.0 = "You are normal!" | weight / height ^ 2 <= 30.0 = "You are fat!" | otherwise = "You are whale!"
在看两个例子。第一个是实现我们自己的max版本:
max'::(Ord a)=>a->a->a max' a b | a <= b = b | otherwise = a
第二个例子是我们自己的compare函数:
compare':: (Ord a)=>a->a->Ordering a `compare'` b | a == b = EQ | a < b = LT | otherwise = GT
6.where语句
上面计算体重情况的例子中,我们反复几算了好几次bmi的值,这明显是一种浪费。在Java中,我们可以使用变量来存储中间计算结果,在Haskell使用where语句来实现类似的功能。我们把上面的例子改写一下,那么where的用法就一目了然了:
bmiTell'' :: Double->Double->String bmiTell'' weight height | bmi <= 18.5 = "You are underweight!" | bmi <= 25.0 = "You are normal!" | bmi <= 30.0 = "You are fat!" | otherwise = "You are whale!" where bmi = weight / height ^ 2
where语句中定义的变量只会对当前的函数可见,因此不必担心它们会污染别的函数的作用域。要想定义的变量多个函数共用,则需要将这些变量定义为全局变量。where中定义的变量是不会在一个函数中的多个模式中共享的。看下面的例子:
greet :: String -> String greet "Juan" = niceGreeting ++ " Juan!" greet "Fernando" = niceGreeting ++ " Fernando!" greet name = badGreeting ++ " " ++ name where niceGreeting = "Hello! So very nice to see you," badGreeting = "Oh! Pfft. It's you."
这个函数是没办法正常工作的,niceGreeting和badGreeting不能在多个模式之间共享,我们可以修改如下:
badGreeting :: String badGreeting = "Oh! Pfft. It's you." niceGreeting :: String niceGreeting = "Hello! So very nice to see you," greet :: String -> String greet "Juan" = niceGreeting ++ " Juan!" greet "Fernando" = niceGreeting ++ " Fernando!" greet name = badGreeting ++ " " ++ name
我们还可以在where语句中使用模式,我们下面定义一个接收firstname和lastname字符串的函数,并返回全名:
initials :: String->String->String initials firstName lastName = [f] ++ ". " ++ [l] ++ "." where (f:_) = firstName (l:_) = lastName
在where语句中也还可以定义函数,举个例子:
calcBMI::[(Double,Double)]->[Double] calcBMI xs = [bmi w h|(w,h)<-xs] where bmi weight height = weight / height ^ 2
7.let语句
let语句与where有些不同,可以在函数中任何地方定义,但是let绑定的变量非常局部,不能跨越Guard。我们看一个例子:
cylinder::Double->Double->Double cylinder r h = let sideArea = 2 * pi * r * h topArea = pi * r ^ 2 in sideArea + 2 * topArea
这是一个计算圆柱体表面积的函数。let往往采用let…in…语法。where和let最大的区别在于let语句是表达式,let可以这么使用:
*Main> 4 * (let a = 9 in a + 1) + 2 42
let还可以用来定义本地函数:
*Main> [let square x = x * x in (square 5,square 3,square 4)] [(25,9,16)]
用let内联式(inline)的绑定多个变量时:
*Main> (let a = 100; b = 200; c = 300 in a * b *c, let foo="Hey "; bar = "there!" in foo ++ bar) (6000000,"Hey there!")
记住,多个let语句用逗号隔开,let中不同的变量用分号隔开。let还可以使用模式:
*Main> (let (a, b, c) = (1, 2, 3) in a+b+c) * 100 600
let还可以在List Comprehension中使用,我们看这个例子:
calcBMI'::[(Double,Double)]->[Double] calcBMI' xs = [bmi|(w,h)<-xs,let bmi = w/h^2]
之前我们在GHCi中也使用过let,在GHCi中,如果使用let语句中的in被省略,那么let就不能再整个交互式的会话中使用,没被省略则可以使用。
8.case语句
case语句和我们之前说的Guards很类似,和switch-case也很类似,我们之间可以个例子就明白了:
head''::[a]->a head'' xs = case xs of [] -> error "Empty List!" (x:_) -> x
case语句的格式是:
case expression of pattern -> result pattern -> result pattern -> result
以上是这一章的主要内容,学到这里已经感觉Haskell的语法很灵活,语法和Java差别还是比较大的。灵活的代价就是知识点实在是很多。仅仅这三章的内容就感觉有点晕了,学了后面就有点忘了前面,看样子得花点时间把前面的复习复习。温故而知新嘛。所以暂缓更新一周。
ps:终于将博客园的代码样式换成自己喜欢的了,非常感谢@Rollen Holt
save me from myself