第四章 Haskell函數(shù)的語(yǔ)法

2022-08-08 13:47 更新
  • 模式匹配
  • 什么是 Guards

  • 關(guān)鍵字 Where
  • 關(guān)鍵字 Let
  • Case expressions

模式匹配

本章講的就是haskell那套酷酷的語(yǔ)法結(jié)構(gòu),先從模式匹配開(kāi)始。模式匹配通過(guò)檢查數(shù)據(jù)的特定結(jié)構(gòu)來(lái)檢查其是否匹配,并按模式從中取得數(shù)據(jù)。

在定義函數(shù)時(shí),你可以為不同的模式分別定義函數(shù)體,這就讓代碼更加簡(jiǎn)潔易讀。你可以匹配一切數(shù)據(jù)類型---數(shù)字,字符,List,元組,等等。我們弄個(gè)簡(jiǎn)單函數(shù),讓它檢查我們傳給它的數(shù)字是不是7。

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

在調(diào)用lucky時(shí),模式會(huì)從上至下進(jìn)行檢查,一旦有匹配,那對(duì)應(yīng)的函數(shù)體就被應(yīng)用了。這個(gè)模式中的唯一匹配是參數(shù)為7,如果不是7,就轉(zhuǎn)到下一個(gè)模式,它匹配一切數(shù)值并將其綁定為x。這個(gè)函數(shù)完全可以使用if實(shí)現(xiàn),不過(guò)我們?nèi)粢獋€(gè)分辨1到5中的數(shù)字,而無(wú)視其它數(shù)的函數(shù)該怎么辦?要是沒(méi)有模式匹配的話,那可得好大一棵if-else樹(shù)了!

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"  

注意下,如果我們把最后匹配一切的那個(gè)模式挪到最前,它的結(jié)果就全都是"Not between 1 and 5"  了。因?yàn)樗约浩ヅ淞艘磺袛?shù)字,不給后面的模式留機(jī)會(huì)。

記得前面實(shí)現(xiàn)的那個(gè)階乘函數(shù)么?當(dāng)時(shí)是把n的階乘定義成了product [1..n]。也可以寫出像數(shù)學(xué)那樣的遞歸實(shí)現(xiàn),先說(shuō)明0的階乘是1,再說(shuō)明每個(gè)正整數(shù)的階乘都是這個(gè)數(shù)與它前驅(qū)(predecessor)對(duì)應(yīng)的階乘的積。如下便是翻譯到haskell的樣子:

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

這就是我們定義的第一個(gè)遞歸函數(shù)。遞歸在haskell中十分重要,我們會(huì)在后面深入理解。如果拿一個(gè)數(shù)(如3)調(diào)用factorial函數(shù),這就是接下來(lái)的計(jì)算步驟:先計(jì)算3*factorial 2,factorial 2等于2*factorial 1,也就是3*(2*(factorial 1))。factorial 1等于1*factorial 0,好,得3*(2*(1*factorial 0)),遞歸在這里到頭了,嗯---我們?cè)谌f(wàn)能匹配前面有定義,0的階乘是1.于是最終的結(jié)果等于3*(2*(1*1))。若是把第二個(gè)模式放在前面,它就會(huì)捕獲包括0在內(nèi)的一切數(shù)字,這一來(lái)我們的計(jì)算就永遠(yuǎn)都不會(huì)停止了。這便是為什么說(shuō)模式的順序是如此重要:它總是優(yōu)先匹配最符合的那個(gè),最后才是那個(gè)萬(wàn)能的。

模式匹配也會(huì)失敗。假如這個(gè)函數(shù):

charName :: Char -> String   
charName 'a' = "Albert"   
charName 'b' = "Broseph"   
charName 'c' = "Cecil"  

拿個(gè)它沒(méi)有考慮到的字符去調(diào)用它,你就會(huì)看到這個(gè):

ghci> charName 'a'   
"Albert"   
ghci> charName 'b'   
"Broseph"   
ghci> charName 'h'   
"*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName  

它告訴我們說(shuō),這個(gè)模式不夠全面。因此,在定義模式時(shí),一定要留一個(gè)萬(wàn)能匹配的模式,這樣我們的程序就不會(huì)為了不可預(yù)料的輸入而崩潰了。

對(duì)Tuple同樣可以使用模式匹配。寫個(gè)函數(shù),將二維空間中的向量相加該如何?將它們的x項(xiàng)和y項(xiàng)分別相加就是了。如果不了解模式匹配,我們很可能會(huì)寫出這樣的代碼:

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)   
addVectors a b = (fst a + fst b, snd a + snd b)  

嗯,可以運(yùn)行。但有更好的方法,上模式匹配:

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)   
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)  

there we go!好多了!注意,它已經(jīng)是個(gè)萬(wàn)能的匹配了。兩個(gè)addVector的類型都是addVectors:: (Num a) => (a,a) -> (a,a) -> (a,a),我們就能夠保證,兩個(gè)參數(shù)都是序?qū)?Pair)了。

fst和snd可以從序?qū)χ腥〕鲈亍HM(Tripple)呢?嗯,沒(méi)現(xiàn)成的函數(shù),得自己動(dòng)手:

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中一樣。表示我們不關(guān)心這部分的具體內(nèi)容。

說(shuō)到List Comprehension,我想起來(lái)在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]   

一旦模式匹配失敗,它就簡(jiǎn)單挪到下個(gè)元素。

對(duì)list本身也可以使用模式匹配。你可以用[]:來(lái)匹配它。因?yàn)?code>[1,2,3]本質(zhì)就是1:2:3:[]的語(yǔ)法糖。你也可以使用前一種形式,像x:xs這樣的模式可以將list的頭部綁定為x,尾部綁定為xs。如果這list只有一個(gè)元素,那么xs就是一個(gè)空l(shuí)ist。

Note:x:xs這模式的應(yīng)用非常廣泛,尤其是遞歸函數(shù)。不過(guò)它只能匹配長(zhǎng)度大于等于1的list。

如果你要把list的前三個(gè)元素都綁定到變量中,可以使用類似x:y:z:xs這樣的形式。它只能匹配長(zhǎng)度大于等于3的list。

我們已經(jīng)知道了對(duì)list做模式匹配的方法,就實(shí)現(xiàn)個(gè)我們自己的head函數(shù)。

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'  

漂亮!注意下,你若要綁定多個(gè)變量(用_也是如此),我們必須用括號(hào)將其括起。同時(shí)注意下我們用的這個(gè)error函數(shù),它可以生成一個(gè)運(yùn)行時(shí)錯(cuò)誤,用參數(shù)中的字符串表示對(duì)錯(cuò)誤的描述。它會(huì)直接導(dǎo)致程序崩潰,因此應(yīng)謹(jǐn)慎使用??墒菍?duì)一個(gè)空l(shuí)ist取head真的不靠譜哇。

弄個(gè)簡(jiǎn)單函數(shù),讓它用非標(biāo)準(zhǔn)的英語(yǔ)給我們展示list的前幾項(xiàng)。

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  

這個(gè)函數(shù)顧及了空l(shuí)ist,單元素list,雙元素list以及較長(zhǎng)的list,所以這個(gè)函數(shù)很安全。(x:[])(x:y:[])也可以寫作[x][x,y](有了語(yǔ)法糖,我們不必多加括號(hào))。不過(guò)(x:y:_)這樣的模式就不行了,因?yàn)樗ヅ涞膌ist長(zhǎng)度不固定。

我們?cè)肔ist Comprehension實(shí)現(xiàn)過(guò)自己的length函數(shù),現(xiàn)在用模式匹配和遞歸重新實(shí)現(xiàn)它:

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

這與先前寫的那個(gè)factorial函數(shù)很相似。先定義好未知輸入的結(jié)果---空l(shuí)ist,這也叫作邊界條件。再在第二個(gè)模式中將這List分割為頭部和尾部。說(shuō),List的長(zhǎng)度就是其尾部的長(zhǎng)度加1。匹配頭部用的_,因?yàn)槲覀儾⒉魂P(guān)心它的值。同時(shí)也應(yīng)明確,我們顧及了List所有可能的模式:第一個(gè)模式匹配空l(shuí)ist,第二個(gè)匹配任意的非空l(shuí)ist。

看下拿"ham"調(diào)用length'會(huì)怎樣。首先它會(huì)檢查它是否為空List。顯然不是,于是進(jìn)入下一模式。它匹配了第二個(gè)模式,把它分割為頭部和尾部并無(wú)視掉頭部的值,得長(zhǎng)度就是1+length' "am"。ok。以此類推,"am"length就是1+length' "m"。好,現(xiàn)在我們有了1+(1+length' "m")。length' "m"1+length ""(也就是1+length' [])。根據(jù)定義,length' []等于0。最后得1+(1+(1+0))

再實(shí)現(xiàn)sum。我們知道空l(shuí)ist的和是0,就把它定義為一個(gè)模式。我們也知道一個(gè)list的和就是頭部加上尾部的和的和。寫下來(lái)就成了:

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

還有個(gè)東西叫做as模式,就是將一個(gè)名字和@置于模式前,可以在按模式分割什么東西時(shí)仍保留對(duì)其整體的引用。如這個(gè)模式xs@(x:y:ys),它會(huì)匹配出與x:y:ys對(duì)應(yīng)的東西,同時(shí)你也可以方便地通過(guò)xs得到整個(gè)list,而不必在函數(shù)體中重復(fù)x:y:ys??聪逻@個(gè)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模式通常就是為了在較大的模式中保留對(duì)整體的引用,從而減少重復(fù)性的工作。

還有——你不可以在模式匹配中使用++。若有個(gè)模式是(xs++ys),那么這個(gè)List該從什么地方分開(kāi)呢?不靠譜吧。而(xs++[x,y,z])或只一個(gè)(xs++[x])或許還能說(shuō)的過(guò)去,不過(guò)出于list的本質(zhì),這樣寫也是不可以的。

什么是 Guards

模式用來(lái)檢查一個(gè)值是否合適并從中取值,而 guard 則用來(lái)檢查一個(gè)值的某項(xiàng)屬性是否為真。咋一聽(tīng)有點(diǎn)像是 ?if ?語(yǔ)句,實(shí)際上也正是如此。不過(guò)處理多個(gè)條件分支時(shí) guard 的可讀性要高些,并且與模式匹配契合的很好。

guards

在講解它的語(yǔ)法前,我們先看一個(gè)用到 guard 的函數(shù)。它會(huì)依據(jù)你的 BMI 值 (body mass index,身體質(zhì)量指數(shù))來(lái)不同程度地侮辱你。BMI 值即為體重除以身高的平方。如果小于 18.5,就是太瘦;如果在 18.5 到 25 之間,就是正常;25 到 30 之間,超重;如果超過(guò) 30,肥胖。這就是那個(gè)函數(shù)(我們目前暫不為您計(jì)算 BMI,它只是直接取一個(gè) BMI 值)。

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

guard 由跟在函數(shù)名及參數(shù)后面的豎線標(biāo)志,通常他們都是靠右一個(gè)縮進(jìn)排成一列。一個(gè) guard 就是一個(gè)布爾表達(dá)式,如果為真,就使用其對(duì)應(yīng)的函數(shù)體。如果為假,就送去見(jiàn)下一個(gè) guard,如之繼續(xù)。如果我們用 24.3 調(diào)用這個(gè)函數(shù),它就會(huì)先檢查它是否小于等于 18.5,顯然不是,于是見(jiàn)下一個(gè) guard。24.3 小于 25.0,因此通過(guò)了第二個(gè) guard 的檢查,就返回第二個(gè)字串。

在這里則是相當(dāng)?shù)暮?jiǎn)潔,不過(guò)不難想象這在命令式語(yǔ)言中又會(huì)是怎樣的一棵 if-else 樹(shù)。由于 if-else 的大樹(shù)比較雜亂,若是出現(xiàn)問(wèn)題會(huì)很難發(fā)現(xiàn),guard 對(duì)此則十分清楚。

最后的那個(gè) guard 往往都是 ?otherwise?,它的定義就是簡(jiǎn)單一個(gè) ?otherwise = True? ,捕獲一切。這與模式很相像,只是模式檢查的是匹配,而它們檢查的是布爾表達(dá)式 。如果一個(gè)函數(shù)的所有 guard 都沒(méi)有通過(guò)(而且沒(méi)有提供 ?otherwise ?作萬(wàn)能匹配),就轉(zhuǎn)入下一模式。這便是 guard 與模式契合的地方。如果始終沒(méi)有找到合適的 guard 或模式,就會(huì)發(fā)生一個(gè)錯(cuò)誤。

當(dāng)然,guard 可以在含有任意數(shù)量參數(shù)的函數(shù)中使用。省得用戶在使用這函數(shù)之前每次都自己計(jì)算 ?bmi?。我們修改下這個(gè)函數(shù),讓它取身高體重為我們計(jì)算。

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

你可以測(cè)試自己胖不胖。

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

運(yùn)行的結(jié)果是我不太胖。不過(guò)程序卻說(shuō)我很丑。

要注意一點(diǎn),函數(shù)的名字和參數(shù)的后面并沒(méi)有 ?=?。許多初學(xué)者會(huì)造成語(yǔ)法錯(cuò)誤,就是因?yàn)樵诤竺婕由狭?nbsp;?=?。

另一個(gè)簡(jiǎn)單的例子:寫個(gè)自己的 ?max ?函數(shù)。應(yīng)該還記得,它是取兩個(gè)可比較的值,返回較大的那個(gè)。

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

guard 也可以塞在一行里面。但這樣會(huì)喪失可讀性,因此是不被鼓勵(lì)的。即使是較短的函數(shù)也是如此,不過(guò)出于展示,我們可以這樣重寫 ?max'?:

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

這樣的寫法根本一點(diǎn)都不容易讀。

我們?cè)賮?lái)試試用 guard 實(shí)現(xiàn)我們自己的 ?compare ?函數(shù):

myCompare :: (Ord a) => a -> a -> Ordering  
a `myCompare` b  
    | a > b     = GT  
    | a == b    = EQ  
    | otherwise = LT
ghci> 3 `myCompare` 2  
GT
*Note*:通過(guò)反單引號(hào),我們不僅可以以中綴形式調(diào)用函數(shù),也可以在定義函數(shù)的時(shí)候使用它。有時(shí)這樣會(huì)更易讀。

關(guān)鍵字 Where

前一節(jié)中我們寫了這個(gè) ?bmi ?計(jì)算函數(shù):

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

注意,我們重復(fù)了 3 次。我們重復(fù)了 3 次。程序員的字典里不應(yīng)該有"重復(fù)"這個(gè)詞。既然發(fā)現(xiàn)有重復(fù),那么給它一個(gè)名字來(lái)代替這三個(gè)表達(dá)式會(huì)更好些。嗯,我們可以這樣修改:

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 = "You're fat! Lose some weight, fatty!"  
    | otherwise   = "You're a whale, congratulations!"  
    where bmi = weight / height ^ 2

我們的 where 關(guān)鍵字跟在 guard 后面(最好是與豎線縮進(jìn)一致),可以定義多個(gè)名字和函數(shù)。這些名字對(duì)每個(gè) guard 都是可見(jiàn)的,這一來(lái)就避免了重復(fù)。如果我們打算換種方式計(jì)算 bmi,只需進(jìn)行一次修改就行了。通過(guò)命名,我們提升了代碼的可讀性,并且由于 bmi 只計(jì)算了一次,函數(shù)的執(zhí)行效率也有所提升。我們可以再做下修改:

bmiTell :: (RealFloat a) => a -> a -> String  
bmiTell weight height  
    | bmi <= skinny = "You're underweight, you emo, you!"  
    | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"  
    | bmi <= fat    = "You're fat! Lose some weight, fatty!"  
    | otherwise     = "You're a whale, congratulations!"  
    where bmi = weight / height ^ 2  
          skinny = 18.5  
          normal = 25.0  
          fat = 30.0

函數(shù)在 where 綁定中定義的名字只對(duì)本函數(shù)可見(jiàn),因此我們不必?fù)?dān)心它會(huì)污染其他函數(shù)的命名空間。注意,其中的名字都是一列垂直排開(kāi),如果不這樣規(guī)范,Haskell 就搞不清楚它們?cè)谀膫€(gè)地方了。

where 綁定不會(huì)在多個(gè)模式中共享。如果你在一個(gè)函數(shù)的多個(gè)模式中重復(fù)用到同一名字,就應(yīng)該把它置于全局定義之中。

where 綁定也可以使用模式匹配!前面那段代碼可以改成:

...  
where bmi = weight / height ^ 2  
      (skinny, normal, fat) = (18.5, 25.0, 30.0)

我們?cè)俑銈€(gè)簡(jiǎn)單函數(shù),讓它告訴我們姓名的首字母:

initials :: String -> String -> String  
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."  
    where (f:_) = firstname  
          (l:_) = lastname

我們完全按可以在函數(shù)的參數(shù)上直接使用模式匹配(這樣更短更簡(jiǎn)潔),在這里只是為了演示在 where 語(yǔ)句中同樣可以使用模式匹配:

where 綁定可以定義名字,也可以定義函數(shù)。保持健康的編程語(yǔ)言風(fēng)格,我們搞個(gè)計(jì)算一組 bmi 的函數(shù):

calcBmis :: (RealFloat a) => [(a, a)] -> [a]  
calcBmis xs = [bmi w h | (w, h) <- xs] 
    where bmi weight height = weight / height ^ 2

這就全了!在這里將 bmi 搞成一個(gè)函數(shù),是因?yàn)槲覀儾荒芤罁?jù)參數(shù)直接進(jìn)行計(jì)算,而必須先從傳入函數(shù)的 List 中取出每個(gè)序?qū)Σ⒂?jì)算對(duì)應(yīng)的值。

where 綁定還可以一層套一層地來(lái)使用。 有個(gè)常見(jiàn)的寫法是,在定義一個(gè)函數(shù)的時(shí)候也寫幾個(gè)輔助函數(shù)擺在 where 綁定中。 而每個(gè)輔助函數(shù)也可以透過(guò) where 擁有各自的輔助函數(shù)。

關(guān)鍵字 Let

let 綁定與 where 綁定很相似。where 綁定是在函數(shù)底部定義名字,對(duì)包括所有 guard 在內(nèi)的整個(gè)函數(shù)可見(jiàn)。let 綁定則是個(gè)表達(dá)式,允許你在任何位置定義局部變量,而對(duì)不同的 guard 不可見(jiàn)。正如 Haskell 中所有賦值結(jié)構(gòu)一樣,let 綁定也可以使用模式匹配??聪滤膶?shí)際應(yīng)用!這是個(gè)依據(jù)半徑和高度求圓柱體表面積的函數(shù):

cylinder :: (RealFloat a) => a -> a -> a  
cylinder r h = 
    let sideArea = 2 * pi * r * h  
        topArea = pi * r ^2  
    in  sideArea + 2 * topArea

letitbe

let 的格式為 let [bindings] in [expressions]。在 let 中綁定的名字僅對(duì) in 部分可見(jiàn)。let 里面定義的名字也得對(duì)齊到一列。不難看出,這用 where 綁定也可以做到。那么它倆有什么區(qū)別呢?看起來(lái)無(wú)非就是,let 把綁定放在語(yǔ)句前面而 where 放在后面嘛。

不同之處在于,let 綁定本身是個(gè)表達(dá)式,而 where 綁定則是個(gè)語(yǔ)法結(jié)構(gòu)。還記得前面我們講if語(yǔ)句時(shí)提到它是個(gè)表達(dá)式,因而可以隨處安放?

ghci> [if 5 > 3 then "Woo" else "Boo", if 'a' > 'b' then "Foo" else "Bar"]  
["Woo", "Bar"]  
ghci> 4 * (if 10 > 5 then 10 else 0) + 2  
42

let 綁定也可以實(shí)現(xiàn):

ghci> 4 * (let a = 9 in a + 1) + 2  
42

let 也可以定義局部函數(shù):

ghci> [let square x = x * x in (square 5, square 3, square 2)]  
[(25,9,4)]

若要在一行中綁定多個(gè)名字,再將它們排成一列顯然是不可以的。不過(guò)可以用分號(hào)將其分開(kāi)。

ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)  
(6000000,"Hey there!")

最后那個(gè)綁定后面的分號(hào)不是必須的,不過(guò)加上也沒(méi)關(guān)系。如我們前面所說(shuō),你可以在 let 綁定中使用模式匹配。這在從 Tuple 取值之類的操作中很方便。

ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100  
600

你也可以把 let 綁定放到 List Comprehension 中。我們重寫下那個(gè)計(jì)算 bmi 值的函數(shù),用個(gè) let 替換掉原先的 where

calcBmis :: (RealFloat a) => [(a, a)] -> [a]  
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]

List Comprehension 中 let 綁定的樣子和限制條件差不多,只不過(guò)它做的不是過(guò)濾,而是綁定名字。let 中綁定的名字在輸出函數(shù)及限制條件中都可見(jiàn)。這一來(lái)我們就可以讓我們的函數(shù)只返回胖子的 bmi 值:

calcBmis :: (RealFloat a) => [(a, a)] -> [a]  
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]

(w, h) <- xs 這里無(wú)法使用 bmi 這名字,因?yàn)樗?let 綁定的前面。

在 List Comprehension 中我們忽略了 let 綁定的 in 部分,因?yàn)槊值目梢?jiàn)性已經(jīng)預(yù)先定義好了。不過(guò),把一個(gè) let...in 放到限制條件中也是可以的,這樣名字只對(duì)這個(gè)限制條件可見(jiàn)。在 ghci 中 in 部分也可以省略,名字的定義就在整個(gè)交互中可見(jiàn)。

ghci> let zoot x y z = x * y + z  
ghci> zoot 3 9 2  
29  
ghci> let boot x y z = x * y + z in boot 3 4 2  
14  
ghci> boot  
< interactive>:1:0: Not in scope: `boot'

你說(shuō)既然 let 已經(jīng)這么好了,還要 where 干嘛呢?嗯,let 是個(gè)表達(dá)式,定義域限制的相當(dāng)小,因此不能在多個(gè) guard 中使用。一些朋友更喜歡 where,因?yàn)樗歉诤瘮?shù)體后面,把主函數(shù)體距離型別聲明近一些會(huì)更易讀。

Case expressions

case

有命令式編程語(yǔ)言 (C, C++, Java, etc.) 的經(jīng)驗(yàn)的同學(xué)一定會(huì)有所了解,很多命令式語(yǔ)言都提供了 case 語(yǔ)句。就是取一個(gè)變量,按照對(duì)變量的判斷選擇對(duì)應(yīng)的代碼塊。其中可能會(huì)存在一個(gè)萬(wàn)能匹配以處理未預(yù)料的情況。

Haskell 取了這一概念融合其中。如其名,case 表達(dá)式就是,嗯,一種表達(dá)式。跟 if..elselet 一樣的表達(dá)式。用它可以對(duì)變量的不同情況分別求值,還可以使用模式匹配。Hmm,取一個(gè)變量,對(duì)它模式匹配,執(zhí)行對(duì)應(yīng)的代碼塊。好像在哪兒聽(tīng)過(guò)?啊,就是函數(shù)定義時(shí)參數(shù)的模式匹配!好吧,模式匹配本質(zhì)上不過(guò)就是 case 語(yǔ)句的語(yǔ)法糖而已。這兩段代碼就是完全等價(jià)的:

head' :: [a] -> a  
head' [] = error "No head for empty lists!"  
head' (x:_) = x
head' :: [a] -> a  
head' xs = case xs of [] -> error "No head for empty lists!"  
                      (x:_) -> x

看得出,case表達(dá)式的語(yǔ)法十分簡(jiǎn)單:

case expression of pattern -> result  
                   pattern -> result  
                   pattern -> result  
                   ...

expression 匹配合適的模式。 一如預(yù)期地,第一個(gè)模式若匹配,就執(zhí)行第一個(gè)區(qū)塊的代碼;否則就接下去比對(duì)下一個(gè)模式。如果到最后依然沒(méi)有匹配的模式,就會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤。

函數(shù)參數(shù)的模式匹配只能在定義函數(shù)時(shí)使用,而 ?case ?表達(dá)式可以用在任何地方。例如:

describeList :: [a] -> String  
describeList xs = "The list is " ++ case xs of [] -> "empty."  
                                               [x] -> "a singleton list."   
                                               xs -> "a longer list."

這在表達(dá)式中作模式匹配很方便,由于模式匹配本質(zhì)上就是 case 表達(dá)式的語(yǔ)法糖,那么寫成這樣也是等價(jià)的:

describeList :: [a] -> String  
describeList xs = "The list is " ++ what xs  
    where what [] = "empty."  
          what [x] = "a singleton list."  
          what xs = "a longer list."


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)