Haskell函數的語法

本章講的就是 Haskell 那套獨特的語法結構,先從模式匹配開始。模式匹配經過檢查數據的特定結構來檢查其是否匹配,並按模式從中取得數據。html

在定義函數時,你能夠爲不一樣的模式分別定義函數自己,這就讓代碼更加簡潔易讀。你能夠匹配一切數據類型 --- 數字,字符,List,元組,等等。咱們弄個簡單函數,讓它檢查咱們傳給它的數字是否是 7。程序員

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

在調用 lucky 時,模式會從上至下進行檢查,一旦有匹配,那對應的函數體就被應用了。這個模式中的惟一匹配是參數爲 7,若是不是 7,就轉到下一個模式,它匹配一切數值並將其綁定爲x 。這個函數徹底可使用 if 實現,不過咱們若要個分辨 1 到 5 中的數字,而無視其它數的函數該怎麼辦?要是沒有模式匹配的話,那可得好大一棵 if-else 樹了!express

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"  

注意下,若是咱們把最後匹配一切的那個模式挪到最前,它的結果就全都是 "Not between 1 and 5"   了。由於它本身匹配了一切數字,不給後面的模式留機會。編程

記得前面實現的那個階乘函數麼?當時是把 n 的階乘定義成了 product [1..n]。也能夠寫出像數學那樣的遞歸實現,先說明 0 的階乘是 1 ,再說明每一個正整數的階乘都是這個數與它前驅 (predecessor) 對應的階乘的積。以下即是翻譯到 Haskell 的樣子:api

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

這就是咱們定義的第一個遞歸函數。遞歸在 Haskell 中十分重要,咱們會在後面深刻理解。若是拿一個數(如 3)調用factorial 函數,這就是接下來的計算步驟:先計算 3*factorial 2factorial 2 等於 2*factorial 1,也就是 3*(2*(factorial 1))factorial 1 等於 1*factorial 0,好,得 3*(2*(1*factorial 0)),遞歸在這裏到頭了,嗯 --- 咱們在萬能匹配前面有定義,0 的階乘是 1.因而最終的結果等於 3*(2*(1*1))。如果把第二個模式放在前面,它就會捕獲包括 0 在內的一切數字,這一來咱們的計算就永遠都不會中止了。這即是爲何說模式的順序是如此重要:它老是優先匹配最符合的那個,最後纔是那個萬能的。安全

模式匹配也會失敗。假如這個函數:編程語言

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

拿個它沒有考慮到的字符去調用它,你就會看到這個:ide

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

它告訴咱們說,這個模式不夠全面。所以,在定義模式時,必定要留一個萬能匹配的模式,這樣咱們的進程就不會爲了避免可預料的輸入而崩潰了。函數

對 Tuple 一樣可使用模式匹配。寫個函數,將二維空間中的矢量相加該如何?將它們的 x 項和 y 項分別相加就是了。若是不瞭解模式匹配,咱們極可能會寫出這樣的代碼:oop

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

嗯,能夠運行。但有更好的方法,上模式匹配:

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

there we go!好多了!注意,它已是個萬能的匹配了。兩個 addVector 的類型都是addVectors:: (Num a) => (a,a) -> (a,a) -> (a,a),咱們就可以保證,兩個參數都是序對 (Pair) 了。

fst 和 snd 能夠從序對中取出元素。三元組 (Tripple) 呢?嗯,沒現成的函數,得本身動手:

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 中同樣。表示咱們不關心這部分的具體內容。

說到 List Comprehension,我想起來在 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]

一旦模式匹配失敗,它就簡單挪到下個元素。

對 List 自己也可使用模式匹配。你能夠用 [] 或 : 來匹配它。由於 [1,2,3] 本質就是 1:2:3:[] 的語法糖。你也可使用前一種形式,像 x:xs 這樣的模式能夠將 List 的頭部綁定爲 x,尾部綁定爲 xs。若是這 List 只有一個元素,那麼 xs 就是一個空 List。

Notex:xs 這模式的應用很是普遍,尤爲是遞歸函數。不過它只能匹配長度大於等於 1 的 List

若是你要把 List 的前三個元素都綁定到變量中,可使用相似 x:y:z:xs 這樣的形式。它只能匹配長度大於等於 3 的 List。

咱們已經知道了對 List 作模式匹配的方法,就實現個咱們本身的 head 函數。

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'  

漂亮!注意下,你若要綁定多個變量(用 _ 也是如此),咱們必須用括號將其括起。同時注意下咱們用的這個 error 函數,它能夠生成一個運行時錯誤,用參數中的字串表示對錯誤的描述。它會直接致使進程崩潰,所以應謹慎使用。但是對一個空 List 取head 真的不靠譜哇。

弄個簡單函數,讓它用非標準的英語給咱們展現 List 的前幾項。

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  

這個函數顧及了空 List,單元素 List,雙元素 List 以及較長的 List,因此這個函數很安全。(x:[]) 與 (x:y:[]) 也能夠寫做 [x] 和 [x,y] (有了語法糖,咱們沒必要多加括號)。不過 (x:y:_) 這樣的模式就不行了,由於它匹配的 List 長度不固定。

咱們曾用 List Comprehension 實現過本身的 length 函數,如今用模式匹配和遞歸從新實現它:

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

這與先前寫的那個 factorial 函數很類似。先定義好未知輸入的結果 --- 空 List,這也叫做邊界條件。再在第二個模式中將這 List 分割爲頭部和尾部。說,List 的長度就是其尾部的長度加 1。匹配頭部用的 _,由於咱們並不關心它的值。同時也應明確,咱們顧及了 List 全部可能的模式:第一個模式匹配空 List,第二個匹配任意的非空 List。

看下拿 "ham" 調用 length' 會怎樣。首先它會檢查它是否爲空 List。顯然不是,因而進入下一模式。它匹配了第二個模式,把它分割爲頭部和尾部並沒有視掉頭部的值,得長度就是 1+length' "am"。ok。以此類推,"am" 的 length 就是1+length' "m"。好,如今咱們有了 1+(1+length' "m")length' "m" 即 1+length "" (也就是1+length' [] )。根據定義,length' [] 等於 0。最後得 1+(1+(1+0))

再實現 sum。咱們知道空 List 的和是 0,就把它定義爲一個模式。咱們也知道一個 List 的和就是頭部加上尾部的和的和。寫下來就成了:

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

還有個東西叫作 as 模式,就是將一個名字和 @ 置於模式前,能夠在按模式分割什麼東西時仍保留對其總體的引用。如這個模式 xs@(x:y:ys),它會匹配出與 x:y:ys 對應的東西,同時你也能夠方便地經過 xs 獲得整個 List,而沒必要在函數體中重複 x:y:ys。看下這個 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 模式一般就是爲了在較大的模式中保留對總體的引用,從而減小重複性的工做。

還有——你不能夠在模式匹配中使用 ++。如有個模式是 (xs++ys),那麼這個 List 該從什麼地方分開呢?不靠譜吧。而(xs++[x,y,z]) 或只一個 (xs++[x]) 或許還能說的過去,不過出於 List 的本質,這樣寫也是不能夠的。

什麼是 Guards

模式用來檢查一個值是否合適並從中取,而 guard 則用來檢查一個值的某項屬性是否爲真。咋一聽有點像是 if 語句,實際上也正是如此。不過處理多個條件分支時 guard 的可讀性要高些,而且與模式匹配契合的很好

 

在講解它的語法前,咱們先看一個用到 guard 的函數。它會依據你的 BMI 值 (body mass index,身體質量指數)來不一樣程度地侮辱你。BMI 值即爲體重除以身高的平方。若是小於 18.5,就是太瘦;若是在 18.5 到 25 之間,就是正常;25 到 30 之間,超重;若是超過 30,肥胖。這就是那個函數(咱們目前暫不爲您計算 bmi,它只是直接取一個 emi 值)。

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 由跟在函數名及參數後面的豎線標誌,一般他們都是靠右一個縮進排成一列。一個 guard 就是一個布爾表達式,若是爲真,就使用其對應的函數體。若是爲假,就送去見下一個 guard,如之繼續。若是咱們用 24.3 調用這個函數,它就會先檢查它是否小於等於 18.5,顯然不是,因而見下一個 guard。24.3 小於 25.0,所以經過了第二個 guard 的檢查,就返回第二個字串。

在這裏則是至關的簡潔,不過不難想象這在命令式語言中又會是怎樣的一棵 if-else 樹。因爲 if-else 的大樹比較雜亂,如果出現問題會很難發現,guard 對此則十分清楚。

最後的那個 guard 每每都是 otherwise,它的定義就是簡單一個 otherwise = True ,捕獲一切。這與模式很相像,只是模式檢查的是匹配,而它們檢查的是布爾表達式 。若是一個函數的全部 guard 都沒有經過(並且沒有提供 otherwise 做萬能匹配),就轉入下一模式。這即是 guard 與模式契合的地方。若是始終沒有找到合適的 guard 或模式,就會發生一個錯誤。

固然,guard 能夠在含有任意數量參數的函數中使用。免得用戶在使用這函數以前每次都本身計算 bmi。咱們修改下這個函數,讓它取身高體重爲咱們計算。

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

你能夠測試本身胖不胖。

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

運行的結果是我不太胖。不過程序卻說我很醜。

要注意一點,函數的名字和參數的後面並無 =。許多初學者會形成語法錯誤,就是由於在後面加上了 =

另外一個簡單的例子:寫個本身的 max 函數。應該還記得,它是取兩個可比較的值,返回較大的那個。

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

guard 也能夠塞在一行裏面。但這樣會喪失可讀性,所以是不被鼓勵的。即便是較短的函數也是如此,不過出於展現,咱們能夠這樣重寫 max'

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

這樣的寫法根本一點都不容易讀。

咱們再來試試用 guard 實現咱們本身的 compare 函數:

myCompare :: (Ord a) => a -> a -> Ordering   
`myCompare` b   
    | a > b     = GT   
    | a == b    = EQ   
    | otherwise = LT  
ghci> 3 `myCompare` 2   
GT  

Note:經過反單引號,咱們不只能夠以中綴形式調用函數,也能夠在定義函數的時候使用它。有時這樣會更易讀。

關鍵字 Where

前一節中咱們寫了這個 bmi 計算函數:

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

注意,咱們重複了 3 次。咱們重複了 3 次。程序員的字典裏不該該有"重複"這個詞。既然發現有重複,那麼給它一個名字來代替這三個表達式會更好些。嗯,咱們能夠這樣修改:

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 關鍵字跟在 guard 後面(最好是與豎線縮進一致),能夠定義多個名字和函數。這些名字對每一個 guard 都是可見的,這一來就避免了重複。若是咱們打算換種方式計算 bmi,只需進行一次修改就好了。經過命名,咱們提高了代碼的可讀性,而且因爲 bmi 只計算了一次,函數的執行效率也有所提高。咱們能夠再作下修改:

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

函數在 where 綁定中定義的名字只對本函數可見,所以咱們沒必要擔憂它會污染其餘函數的命名空間。注意,其中的名字都是一列垂直排開,若是不這樣規範,Haskell 就搞不清楚它們在哪一個地方了。

where 綁定不會在多個模式中共享。若是你在一個函數的多個模式中重複用到同一名字,就應該把它置於全局定義之中。

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

...   
where bmi = weight / height ^ 2   
      (skinny, normal, fat) = (18.525.030.0)  

咱們再搞個簡單函數,讓它告訴咱們姓名的首字母:

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

咱們徹底按能夠在函數的參數上直接使用模式匹配(這樣更短更簡潔),在這裏只是爲了演示在 where 語句中一樣可使用模式匹配:

where 綁定能夠定義名字,也能夠定義函數。保持健康的編程語言風格,咱們搞個計算一組 bmi 的函數:

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

這就全了!在這裏將 bmi 搞成一個函數,是由於咱們不能依據參數直接進行計算,而必須先從傳入函數的 List 中取出每一個序對並計算對應的值。

where 綁定還能夠一層套一層地來使用。

有個常見的寫法是,在定義一個函數的時候也寫幾個輔助函數擺在 where 綁定中。

而每一個輔助函數也能夠透過 where 擁有各自的輔助函數。

關鍵字 Let

let 綁定與 where 綁定很類似。where 綁定是在函數底部定義名字,對包括全部 guard 在內的整個函數可見。let綁定則是個表達式,容許你在任何位置定義局部變量,而對不一樣的 guard 不可見。如 Haskell 中全部賦值結構同樣,let 綁定也可使用模式匹配。看下它的實際應用!這是個依據半徑和高度求圓柱體表面積的函數:

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

let 的格式爲 let [bindings] in [expressions]。在 let 中綁定的名字僅對 in 部分可見。let 裏面定義的名字也得對齊到一列。不難看出,這用where 綁定也能夠作到。那麼它倆有什麼區別呢?看起來無非就是,let 把綁定放在語句前面而 where 放在後面嘛。

不一樣之處在於,let 綁定自己是個表達式,而 where 綁定則是個語法結構。還記得前面咱們講if語句時提到它是個表達式,於是能夠隨處安放?

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 綁定也能夠實現:

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

let 也能夠定義局部函數:

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

若要在一行中綁定多個名字,再將它們排成一列顯然是不能夠的。不過能夠用分號將其分開。

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

最後那個綁定後面的分號不是必須的,不過加上也不要緊。如咱們前面所說,你能夠在 let 綁定中使用模式匹配。這在從 Tuple 取值之類的操做中很方便。

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

你也可以把 let 綁定放到 List Comprehension 中。咱們重寫下那個計算 bmi 值的函數,用個 let 替換掉原先的where

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

List Comprehension 中 let 綁定的樣子和限制條件差很少,只不過它作的不是過濾,而是綁定名字let 中綁定的名字在輸出函數及限制條件中均可見。這一來咱們就可讓咱們的函數只返回胖子的 bmi 值:

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

在 (w, h) <- xs 這裏沒法使用 bmi 這名字,由於它在 let 綁定的前面。

在 List Comprehension 中咱們忽略了 let 綁定的 in 部分,由於名字的可見性已經預先定義好了。不過,把一個let...in 放到限制條件中也是能夠的,這樣名字只對這個限制條件可見。在 ghci 中 in 部分也能夠省略,名字的定義就在整個交互中可見。

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:0Not in scope: `boot'

你說既然 let 已經這麼好了,還要 where 幹嗎呢?嗯,let 是個表達式,定義域限制的至關小,所以不能在多個 guard 中使用。一些朋友更喜歡 where,由於它是跟在函數體後面,把主函數體距離類型聲明近一些會更易讀。

 

Case expressions

有命令式編程語言 (C, C++, Java, etc.) 的經驗的同窗必定會有所瞭解,不少命令式語言都提供了 case 語句。就是取一個變量,按照對變量的判斷選擇對應的代碼塊。其中可能會存在一個萬能匹配以處理未預料的狀況。

Haskell 取了這一律念融合其中。如其名case 表達式就是,嗯,一種表達式。跟if..else 和 let 同樣的表達式。用它能夠對變量的不一樣狀況分別求值,還可使用模式匹配。Hmm,取一個變量,對它模式匹配,執行對應的代碼塊。好像在哪兒聽過?啊,就是函數定義時參數的模式匹配!好吧,模式匹配本質上不過就是 case 語句的語法糖而已。這兩段代碼就是徹底等價的:

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表達式的語法十分簡單:

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

expression 匹配合適的模式。

一如預期地,第一個模式若匹配,就執行第一個區塊的代碼;不然就接下去比對下一個模式。若是到最後依然沒有匹配的模式,就會產生運行時錯誤。

函數參數的模式匹配只能在定義函數時使用,而 case 表達式能夠用在任何地方。例如:

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

這在表達式中做模式匹配很方便,因爲模式匹配本質上就是 case 表達式的語法糖,那麼寫成這樣也是等價的:

describeList :: [a] -> String   
describeList xs = "The list is " ++ what xs   
    where what [] = "empty."   
          what [x] = "a singleton list."   
          what xs = "a longer list."  轉自:http://learnyouahaskell-zh-tw.csie.org/zh-cn/syntax-on-function.html
相關文章
相關標籤/搜索