Haskell 入门笔记(五)

Learn Haskell(五)
这一部分主要讲Haskell的函数语法。

模式匹配(Pattern Match)

模式匹配主要用来定义一些数据必须遵循的规则,根据他们来解析数据。在定义函数的时候,可以为不同的模式定义不同的函数体,以便写出可读性较高的代码。Haskell允许对很多种类型进行模式匹配,数值型字符列表元组等等。下面是一个函数用来检查输入参数是不是7:

lucky::Int->String
lucky 7 = "LUCKY NUMBER SEVEN"
lucky x = "Sorry, you are out of lucky, pal!" 

把上面的代码保存为 lucky.hs 文件。然后在 WinGHCi 中选择 File -> Load -> lucky.hs。你会在 WinGHCi 命令提示符窗口中看到其实际执行了如下命令:

:cd C:\Users\Administrator\Desktop
:load "lucky.hs"

我们试着在命令提示符窗口中调用一下上面的函数:

*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!谁叫他是全匹配模式(ctach all, 捕获所有)呢?无论输入什么值,他都能匹配上,程序当然就没办法往下执行。

我们之前提到过用 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
-- 异常:在函数 charName 中有非详尽模式。 
*Main> charName 'a'
"Albert" 

我们的函数中根本没定义能够匹配字符h的模式,当输入h时,程序不知道该怎么处理,自然会报错。这个例子说明,使用模式匹配的时候,必须定义一个catch all 模式,这样才能应对可能出现的奇奇怪怪的输入。其实 Haskell 中的模式匹配和 Java 中的 switch-case 语句很像,每一个模式就是一个 case,最后还别忘了定义一个 default。但是模式匹配的功能肯定比 Java 的 switch-case 语句强大。Java 中,switch 语句只能对 int 类型使用,由于 bytecharshort 可以自动类型转换成 int,也勉强可以使用。但是 Haskell 中的模式匹配对元组列表List Comprehension 都有效。我们继续往下看:

元组与模式匹配

假设我们使用一个 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) 来匹配两个元组,只不过这本身就是 catch all 模式,这样改写就很明确了。对于 pair 来说,Haskell 提供了 fstsnd 来分别取第一个或第二个部分的值。但对于 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

这里的 _ 有点类似于占位符,表示我们不关心的某个值而已。

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 模式匹配不能使用 ++ 运算符。

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]

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 

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 

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

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

let还可以在List Comprehension中使用,我们看这个例子:

calcBMI'::[(Double,Double)]->[Double]
calcBMI' xs = [bmi|(w,h)<-xs,let bmi = w/h^2] 

之前我们在GHCi中也使用过let,在GHCi中,如果使用let语句中的in被省略,那么let就不能再整个交互式的会话中使用,没被省略则可以使用。

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差别还是比较大的。灵活的代价就是知识点实在是很多。仅仅这三章的内容就感觉有点晕了,学了后面就有点忘了前面,看样子得花点时间把前面的复习复习。温故而知新嘛。所以暂缓更新一周。

你可能感兴趣的:(Haskell 入门笔记(五))