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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM