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

posted @ 2012-08-19 11:52  wawlian  阅读(5444)  评论(2编辑  收藏  举报