http://wiki.jikexueyuan.com/project/haskell-guide/images/modules.png" alt="" />
Haskell 中的模組是含有一組相關(guān)的函數(shù),型別和型別類的組合。而 Haskell 程序的本質(zhì)便是從主模組中引用其它模組并呼叫其中的函數(shù)來執(zhí)行操作。這樣可以把程式碼分成多塊,只要一個模組足夠的獨(dú)立,它里面的函數(shù)便可以被不同的程序反復(fù)重用。這就讓不同的程式碼各司其職,提高了程式碼的健壯性。
Haskell 的標(biāo)準(zhǔn)庫就是一組模組,每個模組都含有一組功能相近或相關(guān)的函數(shù)和型別。有處理 List 的模組,有處理并發(fā)的模組,也有處理復(fù)數(shù)的模組,等等。目前為止我們談及的所有函數(shù),型別以及型別類都是 Prelude 模組的一部分,它預(yù)設(shè)自動裝載。在本章,我們看一下幾個常用的模組,在開始瀏覽其中的函數(shù)之前,我們先得知道如何裝載模組.
在 Haskell中,裝載模組的語法為 import,這必須得在函數(shù)的定義之前,所以一般都是將它置于程式碼的頂部。無疑,一段程式碼中可以裝載很多模組,只要將 import 語句分行寫開即可。裝載 Data.List 試下,它里面有很多實(shí)用的 List 處理函數(shù).
執(zhí)行 import Data.List,這樣一來 Data.List 中包含的所有函數(shù)就都進(jìn)入了全局命名空間。也就是說,你可以在程式碼的任意位置呼叫這些函數(shù).Data.List 模組中有個 nub 函數(shù),它可以篩掉一個 List 中的所有重復(fù)元素。用點(diǎn)號將 length 和 nub 組合: length . nub,即可得到一個與 (\xs -> length (nub xs)) 等價的函數(shù)。
import Data.List
numUniques :: (Eq a) => [a] -> Int
numUniques = length . nub
你也可以在 ghci 中裝載模組,若要呼叫 Data.List 中的函數(shù),就這樣:
ghci> :m Data.List
若要在 ghci 中裝載多個模組,不必多次 :m 命令,一下就可以全部搞定:
ghci> :m Data.List Data.Map Data.Set
而你的程序中若已經(jīng)有包含的程式碼,就不必再用 :m 了.
如果你只用得到某模組的兩個函數(shù),大可僅包含它倆。若僅裝載 Data.List 模組 nub 和 sort,就這樣:
import Data.List (nub,sort)
也可以只包含除去某函數(shù)之外的其它函數(shù),這在避免多個模組中函數(shù)的命名沖突很有用。假設(shè)我們的程式碼中已經(jīng)有了一個叫做 nub 的函數(shù),而裝入 Data.List 模組時就要把它里面的 nub 除掉.
import Data.List hiding (nub)
避免命名沖突還有個方法,便是 qualified import,Data.Map 模組提供一了一個按鍵索值的資料結(jié)構(gòu),它里面有幾個和 Prelude 模組重名的函數(shù)。如 filter 和 null,裝入 Data.Map 模組之后再呼叫 filter,Haskell 就不知道它究竟是哪個函數(shù)。如下便是解決的方法:
import qualified Data.Map
這樣一來,再呼叫 Data.Map 中的 filter 函數(shù),就必須得 Data.Map.filter,而 filter 依然是為我們熟悉喜愛的樣子。但是要在每個函數(shù)前面都加 個Data.Map 實(shí)在是太煩人了! 那就給它起個別名,讓它短些:
import qualified Data.Map as M
好,再呼叫 Data.Map 模組的 filter 函數(shù)的話僅需 M.filter 就行了
要瀏覽所有的標(biāo)準(zhǔn)庫模組,參考這個手冊。翻閱標(biāo)準(zhǔn)庫中的模組和函數(shù)是提升個人 Haskell 水平的重要途徑。你也可以各個模組的原始碼,這對 Haskell 的深入學(xué)習(xí)及掌握都是大有好處的.
檢索函數(shù)或搜尋函數(shù)位置就用 [http://www.Haskell.org/hoogle/ Hoogle],相當(dāng)了不起的 Haskell 搜索引擎! 你可以用函數(shù)名,模組名甚至型別聲明來作為檢索的條件.
顯而易見,Data.List 是關(guān)于 List 操作的模組,它提供了一組非常有用的 List 處理函數(shù)。在前面我們已經(jīng)見過了其中的幾個函數(shù)(如 map 和 filter),這是 Prelude 模組出于方便起見,導(dǎo)出了幾個 Data.List 里的函數(shù)。因?yàn)檫@幾個函數(shù)是直接引用自 Data.List,所以就無需使用 qualified import。在下面,我們來看看幾個以前沒見過的函數(shù):
intersperse 取一個元素與 List 作參數(shù),并將該元素置于 List 中每對元素的中間。如下是個例子:
ghci> intersperse '.' "MONKEY"
"M.O.N.K.E.Y"
ghci> intersperse 0 [1,2,3,4,5,6]
[1,0,2,0,3,0,4,0,5,0,6]
intercalate 取兩個 List 作參數(shù)。它會將第一個 List 交叉插入第二個 List 中間,并返回一個 List.
ghci> intercalate " " ["hey","there","guys"]
"hey there guys"
ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]]
[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]
transpose 函數(shù)可以反轉(zhuǎn)一組 List 的 List。你若把一組 List 的 List 看作是個 2D 的矩陣,那 transpose 的操作就是將其列為行。
ghci> transpose [[1,2,3],[4,5,6],[7,8,9]]
[[1,4,7],[2,5,8],[3,6,9]]
ghci> transpose ["hey","there","guys"]
["htg","ehu","yey","rs","e"]
假如有兩個多項(xiàng)式 3x<sup>2</sup> + 5x + 9,10x<sup>3</sup> + 9 和 8x<sup>3</sup> + 5x<sup>2</sup> + x - 1,將其相加,我們可以列三個 List: [0,3,5,9],[10,0,0,9] 和 [8,5,1,-1] 來表示。再用如下的方法取得結(jié)果.
ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]
[18,8,6,17]
http://wiki.jikexueyuan.com/project/haskell-guide/images/legolists.png" alt="" />
使用 transpose 處理這三個 List 之后,三次冪就到了第一行,二次冪到了第二行,以此類推。在用 sum 函數(shù)將其映射,即可得到正確的結(jié)果。
foldl' 和 foldl1' 是它們各自惰性實(shí)現(xiàn)的嚴(yán)格版本。在用 fold 處理較大的 List 時,經(jīng)常會遇到堆棧溢出的問題。而這罪魁禍?zhǔn)拙褪?fold 的惰性: 在執(zhí)行 fold 時,累加器的值并不會被立即更新,而是做一個"在必要時會取得所需的結(jié)果"的承諾。每過一遍累加器,這一行為就重復(fù)一次。而所有的這堆"承諾"最終就會塞滿你的堆棧。嚴(yán)格的 fold 就不會有這一問題,它們不會作"承諾",而是直接計(jì)算中間值的結(jié)果并繼續(xù)執(zhí)行下去。如果用惰性 fold 時經(jīng)常遇到溢出錯誤,就應(yīng)換用它們的嚴(yán)格版。
concat 把一組 List 連接為一個 List。
ghci> concat ["foo","bar","car"]
"foobarcar"
ghci> concat [[3,4,5],[2,3,4],[2,1,1]]
[3,4,5,2,3,4,2,1,1]
它相當(dāng)于移除一級嵌套。若要徹底地連接其中的元素,你得 concat 它兩次才行.
concatMap 函數(shù)與 map 一個 List 之后再 concat 它等價.
ghci> concatMap (replicate 4) [1..3]
[1,1,1,1,2,2,2,2,3,3,3,3]
and 取一組布林值 List 作參數(shù)。只有其中的值全為 True 的情況下才會返回 True。
ghci> and $ map (>4) [5,6,7,8]
True
ghci> and $ map (==4) [4,4,4,3,4]
False
or 與 and 相似,一組布林值 List 中若存在一個 True 它就返回 True.
ghci> or $ map (==4) [2,3,4,5,6,1]
True
ghci> or $ map (>4) [1,2,3]
False
any 和 all 取一個限制條件和一組布林值 List 作參數(shù),檢查是否該 List 的某個元素或每個元素都符合該條件。通常較 map 一個 List 到 and 或 or 而言,使用 any 或 all 會更多些。
ghci> any (==4) [2,3,5,6,1,4]
True
ghci> all (>4) [6,9,10]
True
ghci> all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
False
ghci> any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
True
iterate 取一個函數(shù)和一個值作參數(shù)。它會用該值去呼叫該函數(shù)并用所得的結(jié)果再次呼叫該函數(shù),產(chǎn)生一個無限的 List.
ghci> take 10 $ iterate (*2) 1
[1,2,4,8,16,32,64,128,256,512]
ghci> take 3 $ iterate (++ "haha") "haha"
["haha","hahahaha","hahahahahaha"]
splitAt 取一個 List 和數(shù)值作參數(shù),將該 List 在特定的位置斷開。返回一個包含兩個 List 的二元組.
ghci> splitAt 3 "heyman"
("hey","man")
ghci> splitAt 100 "heyman"
("heyman","")
ghci> splitAt (-3) "heyman"
("","heyman")
ghci> let (a,b) = splitAt 3 "foobar" in b ++ a
"barfoo"
takeWhile 這一函數(shù)十分的實(shí)用。它從一個 List 中取元素,一旦遇到不符合條件的某元素就停止.
ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]
[6,5,4]
ghci> takeWhile (/=' ') "This is a sentence"
"This"
如果要求所有三次方小于 1000 的數(shù)的和,用 filter 來過濾 map (^3) [1..] 所得結(jié)果中所有小于 1000 的數(shù)是不行的。因?yàn)閷o限 List 執(zhí)行的 filter 永遠(yuǎn)都不會停止。你已經(jīng)知道了這個 List 是單增的,但 Haskell 不知道。所以應(yīng)該這樣:
ghci> sum $ takeWhile (<10000) $ map (^3) [1..]
53361
用 (^3) 處理一個無限 List,而一旦出現(xiàn)了大于 10000 的元素這個 List 就被切斷了,sum 到一起也就輕而易舉.
dropWhile 與此相似,不過它是扔掉符合條件的元素。一旦限制條件返回 False,它就返回 List 的余下部分。方便實(shí)用!
ghci> dropWhile (/=' ') "This is a sentence"
" is a sentence"
ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1]
[3,4,5,4,3,2,1]
給一 Tuple 組成的 List,這 Tuple 的首項(xiàng)表示股票價格,第二三四項(xiàng)分別表示年,月,日。我們想知道它是在哪天首次突破 $1000 的!
ghci> let stock = [(994.4,2008,9,1),(995.2,2008,9,2),(999.2,2008,9,3),(1001.4,2008,9,4),(998.3,2008,9,5)]
ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock)
(1001.4,2008,9,4)
span 與 takeWhile 有點(diǎn)像,只是它返回兩個 List。第一個 List 與同參數(shù)呼叫 takeWhile 所得的結(jié)果相同,第二個 List 就是原 List 中余下的部分。
ghci> let (fw,rest) = span (/=' ') "This is a sentence" in "First word:" ++ fw ++ ",the rest:" ++ rest
"First word: This,the rest: is a sentence"
span 是在條件首次為 False 時斷開 List,而 break 則是在條件首次為 True 時斷開 List。break p 與 span (not . p) 是等價的.
ghci> break (==4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
ghci> span (/=4) [1,2,3,4,5,6,7]
([1,2,3],[4,5,6,7])
break 返回的第二個 List 就會以第一個符合條件的元素開頭。
sort 可以排序一個 List,因?yàn)橹挥心軌蜃鞅容^的元素才可以被排序,所以這一 List 的元素必須是 Ord 型別類的實(shí)例型別。
ghci> sort [8,5,3,2,1,6,4,2]
[1,2,2,3,4,5,6,8]
ghci> sort "This will be sorted soon"
" Tbdeehiillnooorssstw"
group 取一個 List 作參數(shù),并將其中相鄰并相等的元素各自歸類,組成一個個子 List.
ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]
若在 group 一個 List 之前給它排序就可以得到每個元素在該 List 中的出現(xiàn)次數(shù)。
ghci> map (\l@(x:xs) -> (x,length l)) . group . sort $ [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
[(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]
inits 和 tails 與 init 和 tail 相似,只是它們會遞歸地呼叫自身直到什么都不剩,看:
ghci> inits "w00t"
["","w","w0","w00","w00t"]
ghci> tails "w00t"
["w00t","00t","0t","t",""]
ghci> let w = "w00t" in zip (inits w) (tails w)
[("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]
我們用 fold 實(shí)現(xiàn)一個搜索子 List 的函數(shù):
search :: (Eq a) => [a] -> [a] -> Bool
search needle haystack =
let nlen = length needle
in foldl (\acc x -> if take nlen x == needle then True else acc) False (tails haystack)
首先,對搜索的 List 呼叫 tails,然后遍歷每個 List 來檢查它是不是我們想要的.
由此我們便實(shí)現(xiàn)了一個類似 isInfixOf 的函數(shù),isInfixOf 從一個 List 中搜索一個子 List,若該 List 包含子 List,則返回 True.
ghci> "cat" `isInfixOf` "im a cat burglar"
True
ghci> "Cat" `isInfixOf` "im a cat burglar"
False
ghci> "cats" `isInfixOf` "im a cat burglar"
False
isPrefixOf 與 isSuffixOf 分別檢查一個 List 是否以某子 List 開頭或者結(jié)尾.
ghci> "hey" `isPrefixOf` "hey there!"
True
ghci> "hey" `isPrefixOf` "oh hey there!"
False
ghci> "there!" `isSuffixOf` "oh hey there!"
True
ghci> "there!" `isSuffixOf` "oh hey there"
False
elem 與 notElem 檢查一個 List 是否包含某元素.
partition 取一個限制條件和 List 作參數(shù),返回兩個 List,第一個 List 中包含所有符合條件的元素,而第二個 List 中包含余下的.
ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOBMORGAN","sidneyeddy")
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
([5,6,7],[1,3,3,2,1,0,3])
了解這個與 span 和 break 的差異是很重要的.
ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
("BOB","sidneyMORGANeddy")
span 和 break 會在遇到第一個符合或不符合條件的元素處斷開,而 partition 則會遍歷整個 List。
find 取一個 List 和限制條件作參數(shù),并返回首個符合該條件的元素,而這個元素是個 Maybe 值。在下章,我們將深入地探討相關(guān)的算法和資料結(jié)構(gòu),但在這里你只需了解 Maybe 值是 Just something 或 Nothing 就夠了。與一個 List 可以為空也可以包含多個元素相似,一個 Maybe 可以為空,也可以是單一元素。同樣與 List 類似,一個 Int 型的 List 可以寫作 [Int],Maybe有個 Int 型可以寫作 Maybe Int。先試一下 find 函數(shù)再說.
ghci> find (>4) [1,2,3,4,5,6]
Just 5
ghci> find (>9) [1,2,3,4,5,6]
Nothing
ghci> :t find
find :: (a -> Bool) -> [a] -> Maybe a
注意一下 find 的型別,它的返回結(jié)果為 Maybe a,這與 [a] 的寫法有點(diǎn)像,只是 Maybe 型的值只能為空或者單一元素,而 List 可以為空,一個元素,也可以是多個元素.
想想前面那段找股票的程式碼,head (dropWhile (\(val,y,m,d) -> val < 1000) stock) 。但 head 并不安全! 如果我們的股票沒漲過 $1000 會怎樣? dropWhile 會返回一個空 List,而對空 List 取 head 就會引發(fā)一個錯誤。把它改成 find (\(val,y,m,d) -> val > 1000) stock 就安全多啦,若存在合適的結(jié)果就得到它, 像 Just (1001.4,2008,9,4),若不存在合適的元素(即我們的股票沒有漲到過 $1000),就會得到一個 Nothing.
elemIndex 與 elem 相似,只是它返回的不是布林值,它只是'可能' (Maybe)返回我們找的元素的索引,若這一元素不存在,就返回 Nothing。
ghci> :t elemIndex
elemIndex :: (Eq a) => a -> [a] -> Maybe Int
ghci> 4 `elemIndex` [1,2,3,4,5,6]
Just 3
ghci> 10 `elemIndex` [1,2,3,4,5,6]
Nothing
elemIndices 與 elemIndex 相似,只不過它返回的是 List,就不需要 Maybe 了。因?yàn)椴淮嬖谟每?List 就可以表示,這就與 Nothing 相似了.
ghci> ' ' `elemIndices` "Where are the spaces?"
[5,9,13]
findIndex 與 find 相似,但它返回的是可能存在的首個符合該條件元素的索引。findIndices 會返回所有符合條件的索引.
ghci> findIndex (==4) [5,3,2,1,6,4]
Just 5
ghci> findIndex (==7) [5,3,2,1,6,4]
Nothing
ghci> findIndices (`elem` ['A'..'Z']) "Where Are The Caps?"
[0,6,10,14]
在前面,我們講過了 zip 和 zipWith,它們只能將兩個 List 組到一個二元組數(shù)或二參函數(shù)中,但若要組三個 List 該怎么辦? 好說~ 有 zip3,zip4...,和 zipWith3, zipWith4...直到 7。這看起來像是個 hack,但工作良好。連著組 8 個 List 的情況很少遇到。還有個聰明辦法可以組起無限多個 List,但限于我們目前的水平,就先不談了.
ghci> zipWith3 (\x y z -> x + y + z) [1,2,3] [4,5,2,2] [2,2,3]
[7,9,8]
ghci> zip4 [2,3,3] [2,2,2] [5,5,3] [2,2,2]
[(2,2,5,2),(3,2,5,2),(3,2,3,2)]
與普通的 zip 操作相似,以返回的 List 中長度最短的那個為準(zhǔn).
在處理來自檔案或其它地方的輸入時,lines 會非常有用。它取一個字串作參數(shù)。并返回由其中的每一行組成的 List.
ghci> lines "first line\nsecond line\nthird line"
["first line","second line","third line"]
'\n' 表示unix下的換行符,在 Haskell 的字元中,反斜杠表示特殊字元.
unlines 是 lines 的反函數(shù),它取一組字串的 List,并將其通過 '\n'合并到一塊.
ghci> unlines ["first line","second line","third line"]
"first line\nsecond line\nthird line\n"
words 和 unwords 可以把一個字串分為一組單詞或執(zhí)行相反的操作,很有用.
ghci> words "hey these are the words in this sentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> words "hey these are the words in this\nsentence"
["hey","these","are","the","words","in","this","sentence"]
ghci> unwords ["hey","there","mate"]
"hey there mate"
我們前面講到了 nub,它可以將一個 List 中的重復(fù)元素全部篩掉,使該 List 的每個元素都如雪花般獨(dú)一無二,'nub' 的含義就是'一小塊'或'一部分',用在這里覺得很古怪。我覺得,在函數(shù)的命名上應(yīng)該用更確切的詞語,而避免使用老掉牙的過時詞匯.
ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1]
[1,2,3,4]
ghci> nub "Lots of words and stuff"
"Lots fwrdanu"
delete 取一個元素和 List 作參數(shù),會刪掉該 List 中首次出現(xiàn)的這一元素.
ghci> delete 'h' "hey there ghang!"
"ey there ghang!"
ghci> delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere ghang!"
ghci> delete 'h' . delete 'h' . delete 'h' $ "hey there ghang!"
"ey tere gang!"
\ 表示 List 的差集操作,這與集合的差集很相似,它會從左邊 List 中的元素扣除存在于右邊 List 中的元素一次.
ghci> [1..10] \\ [2,5,9]
[1,3,4,6,7,8,10]
ghci> "Im a big baby" \\ "big"
"Im a baby"
union 與集合的并集也是很相似,它返回兩個 List 的并集,即遍歷第二個 List 若存在某元素不屬于第一個 List,則追加到第一個 List???,第二個 List 中的重復(fù)元素就都沒了!
ghci> "hey man" `union` "man what's up"
"hey manwt'sup"
ghci> [1..7] `union` [5..10]
[1,2,3,4,5,6,7,8,9,10]
intersection 相當(dāng)于集合的交集。它返回兩個 List 的相同部分.
ghci> [1..7] `intersect` [5..10]
[5,6,7]
insert 可以將一個元素插入一個可排序的 List,并將其置于首個大于等于它的元素之前,如果使用 insert 來給一個排過序的 List 插入元素,返回的結(jié)果依然是排序的.
ghci> insert 4 [1,2,3,5,6,7]
[1,2,3,4,5,6,7]
ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z']
"abcdefghijklmnopqrstuvwxyz"
ghci> insert 3 [1,2,4,3,2,1]
[1,2,3,4,3,2,1]
length,take,drop,splitAt,!! 和 replicate 之類的函數(shù)有個共同點(diǎn)。那就是它們的參數(shù)中都有個 Int 值(或者返回Int值),我覺得使用 Intergal 或 Num 型別類會更好,但出于歷史原因,修改這些會破壞掉許多既有的程式碼。在 Data.List 中包含了更通用的替代版,如: genericLength,genericTake,genericDrop,genericSplitAt,genericIndex 和 genericReplicate。length 的型別聲明為 length :: [a] -> Int,而我們?nèi)粢襁@樣求它的平均值,let xs = [1..6] in sum xs / length xs ,就會得到一個型別錯誤,因?yàn)?/ 運(yùn)算符不能對 Int 型使用! 而 genericLength 的型別聲明則為 genericLength :: (Num a) => [b] -> a,Num 既可以是整數(shù)又可以是浮點(diǎn)數(shù),let xs = [1..6] in sum xs / genericLength xs 這樣再求平均數(shù)就不會有問題了.
nub, delete, union, intsect 和 group 函數(shù)也有各自的通用替代版 nubBy,deleteBy,unionBy,intersectBy 和 groupBy,它們的區(qū)別就是前一組函數(shù)使用 (==) 來測試是否相等,而帶 By 的那組則取一個函數(shù)作參數(shù)來判定相等性,group 就與 groupBy (==) 等價.
假如有個記錄某函數(shù)在每秒的值的 List,而我們要按照它小于零或者大于零的交界處將其分為一組子 List。如果用 group,它只能將相鄰并相等的元素組到一起,而在這里我們的標(biāo)準(zhǔn)是它們是否互為相反數(shù)。groupBy 登場! 它取一個含兩個參數(shù)的函數(shù)作為參數(shù)來判定相等性.
ghci> let values = [-4.3,-2.4,-1.2,0.4,2.3,5.9,10.5,29.1,5.3,-2.4,-14.5,2.9,2.3]
ghci> groupBy (\x y -> (x > 0) == (y > 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]
這樣一來我們就可以很清楚地看出哪部分是正數(shù),哪部分是負(fù)數(shù),這個判斷相等性的函數(shù)會在兩個元素同時大于零或同時小于零時返回 True。也可以寫作 \x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0)。但我覺得第一個寫法的可讀性更高。Data.Function 中還有個 on 函數(shù)可以讓它的表達(dá)更清晰,其定義如下:
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
f `on` g = \x y -> f (g x) (g y)
執(zhí)行 (==) `on` (> 0) 得到的函數(shù)就與 \x y -> (x > 0) == (y > 0) 基本等價。on 與帶 By 的函數(shù)在一起會非常好用,你可以這樣寫:
ghci> groupBy ((==) `on` (> 0)) values
[[-4.3,-2.4,-1.2],[0.4,2.3,5.9,10.5,29.1,5.3],[-2.4,-14.5],[2.9,2.3]]
可讀性很高! 你可以大聲念出來: 按照元素是否大于零,給它分類!
同樣,sort,insert,maximum 和 min 都有各自的通用版本。如 groupBy 類似,sortBy,insertBy,maximumBy 和 minimumBy 都取一個函數(shù)來比較兩個元素的大小。像 sortBy 的型別聲明為: sortBy :: (a -> a -> Ordering) -> [a] -> [a]。前面提過,Ordering 型別可以有三個值,LT,EQ 和 GT。compare 取兩個 Ord 型別類的元素作參數(shù),所以 sort 與 sortBy compare 等價.
List 是可以比較大小的,且比較的依據(jù)就是其中元素的大小。如果按照其子 List 的長度為標(biāo)準(zhǔn)當(dāng)如何? 很好,你可能已經(jīng)猜到了,sortBy 函數(shù).
ghci> let xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
ghci> sortBy (compare `on` length) xs
[[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]
太絕了! compare `on` length,乖乖,這簡直就是英文! 如果你搞不清楚 on 在這里的原理,就可以認(rèn)為它與 \x y -> length x `compare` length y 等價。通常,與帶 By 的函數(shù)打交道時,若要判斷相等性,則 (==) `on` something。若要判定大小,則 compare `on` something.
如其名,Data.Char 模組包含了一組用于處理字元的函數(shù)。由于字串的本質(zhì)就是一組字元的 List,所以往往會在 filter 或是 map 字串時用到它.
Data.Char模組中含有一系列用于判定字元范圍的函數(shù),如下:
http://wiki.jikexueyuan.com/project/haskell-guide/images/legochar.png" alt="" />
isControl 判斷一個字元是否是控制字元。 isSpace 判斷一個字元是否是空格字元,包括空格,tab,換行符等. isLower 判斷一個字元是否為小寫. isUper 判斷一個字元是否為大寫。 isAlpha 判斷一個字元是否為字母. isAlphaNum 判斷一個字元是否為字母或數(shù)字. isPrint 判斷一個字元是否是可打印的. isDigit 判斷一個字元是否為數(shù)字. isOctDigit 判斷一個字元是否為八進(jìn)制數(shù)字. isHexDigit 判斷一個字元是否為十六進(jìn)制數(shù)字. isLetter 判斷一個字元是否為字母. isMark 判斷是否為 unicode 注音字元,你如果是法國人就會經(jīng)常用到的. isNumber 判斷一個字元是否為數(shù)字. isPunctuation 判斷一個字元是否為標(biāo)點(diǎn)符號. isSymbol判斷一個字元是否為貨幣符號. isSeperater 判斷一個字元是否為 unicode 空格或分隔符. isAscii 判斷一個字元是否在 unicode 字母表的前 128 位。 isLatin1 判斷一個字元是否在 unicode 字母表的前 256 位. isAsciiUpper 判斷一個字元是否為大寫的 ascii 字元. isAsciiLower 判斷一個字元是否為小寫的 ascii 字元.
以上所有判斷函數(shù)的型別聲明皆為 Char -> Bool,用到它們的絕大多數(shù)情況都無非就是過濾字串或類似操作。假設(shè)我們在寫個程序,它需要一個由字元和數(shù)字組成的用戶名。要實(shí)現(xiàn)對用戶名的檢驗(yàn),我們可以結(jié)合使用 Data.List 模組的 all 函數(shù)與 Data.Char 的判斷函數(shù).
ghci> all isAlphaNum "bobby283"
True
ghci> all isAlphaNum "eddy the fish!"
False
Kewl~ 免得你忘記,all 函數(shù)取一個判斷函數(shù)和一個 List 做參數(shù),若該 List 的所有元素都符合條件,就返回 True.
也可以使用 isSpace 來實(shí)現(xiàn) Data.List 的 words 函數(shù).
ghci> words "hey guys its me"
["hey","guys","its","me"]
ghci> groupBy ((==) `on` isSpace) "hey guys its me"
["hey"," ","guys"," ","its"," ","me"]
ghci>
Hmm,不錯,有點(diǎn) words 的樣子了。只是還有空格在里面,恩,該怎么辦? 我知道,用 filter 濾掉它們!
ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hey guys its me"
["hey","guys","its","me"]
啊哈.
Data.Char 中也含有與 Ordering 相似的型別。Ordering 可以有三個值,LT,GT 和 EQ。這就是個枚舉,它表示了兩個元素作比較可能的結(jié)果. GeneralCategory 型別也是個枚舉,它表示了一個字元可能所在的分類。而得到一個字元所在分類的主要方法就是使用 generalCategory 函數(shù).它的型別為: generalCategory :: Char -> GeneralCategory。那 31 個分類就不在此一一列出了,試下這個函數(shù)先:
ghci> generalCategory ' '
Space
ghci> generalCategory 'A'
UppercaseLetter
ghci> generalCategory 'a'
LowercaseLetter
ghci> generalCategory '.'
OtherPunctuation
ghci> generalCategory '9'
DecimalNumber
ghci> map generalCategory " \t\nA9?|"
[Space,Control,Control,UppercaseLetter,DecimalNumber,OtherPunctuation,MathSymbol]
由于 GeneralCategory 型別是 Eq 型別類的一部分,使用類似 generalCategory c == Space 的程式碼也是可以的.
toUpper 將一個字元轉(zhuǎn)為大寫字母,若該字元不是小寫字母,就按原值返回.
toLower 將一個字元轉(zhuǎn)為小寫字母,若該字元不是大寫字母,就按原值返回.
toTitle 將一個字元轉(zhuǎn)為 title-case,對大多數(shù)字元而言,title-case 就是大寫.
digitToInt 將一個字元轉(zhuǎn)為 Int 值,而這一字元必須得在 '1'..'9','a'..'f'或'A'..'F' 的范圍之內(nèi).
ghci> map digitToInt "34538"
[3,4,5,3,8]
ghci> map digitToInt "FF85AB"
[15,15,8,5,10,11]
intToDigit 是 digitToInt 的反函數(shù)。它取一個 0 到 15 的 Int 值作參數(shù),并返回一個小寫的字元.
ghci> intToDigit 15
'f'
ghci> intToDigit 5
'5'
ord 與 char 函數(shù)可以將字元與其對應(yīng)的數(shù)字相互轉(zhuǎn)換.
ghci> ord 'a'
97
ghci> chr 97
'a'
ghci> map ord "abcdefgh"
[97,98,99,100,101,102,103,104]
兩個字元的 ord 值之差就是它們在 unicode 字元表上的距離.
Caesar ciphar 是加密的基礎(chǔ)算法,它將消息中的每個字元都按照特定的字母表進(jìn)行替換。它的實(shí)現(xiàn)非常簡單,我們這里就先不管字母表了.
encode :: Int -> String -> String
encode shift msg =
let ords = map ord msg
shifted = map (+ shift) ords
in map chr shifted
先將一個字串轉(zhuǎn)為一組數(shù)字,然后給它加上某數(shù),再轉(zhuǎn)回去。如果你是標(biāo)準(zhǔn)的組合牛仔,大可將函數(shù)寫為: map (chr . (+ shift) . ord) msg。試一下它的效果:
ghci> encode 3 "Heeeeey"
"Khhhhh|"
ghci> encode 4 "Heeeeey"
"Liiiii}"
ghci> encode 1 "abcd"
"bcde"
ghci> encode 5 "Marry Christmas! Ho ho ho!"
"Rfww~%Hmwnxyrfx&%Mt%mt%mt&"
不錯。再簡單地將它轉(zhuǎn)成一組數(shù)字,減去某數(shù)后再轉(zhuǎn)回來就是解密了.
decode :: Int -> String -> String
decode shift msg = encode (negate shift) msg
ghci> encode 3 "Im a little teapot"
"Lp#d#olwwoh#whdsrw"
ghci> decode 3 "Lp#d#olwwoh#whdsrw"
"Im a little teapot"
ghci> decode 5 . encode 5 $ "This is a sentence"
"This is a sentence"
關(guān)聯(lián)列表(也叫做字典)是按照鍵值對排列而沒有特定順序的一種 List。例如,我們用關(guān)聯(lián)列表儲存電話號碼,號碼就是值,人名就是鍵。我們并不關(guān)心它們的存儲順序,只要能按人名得到正確的號碼就好.在 Haskell 中表示關(guān)聯(lián)列表的最簡單方法就是弄一個二元組的 List,而這二元組就首項(xiàng)為鍵,后項(xiàng)為值。如下便是個表示電話號碼的關(guān)聯(lián)列表:
phoneBook = [("betty","555-2938") ,
("bonnie","452-2928") ,
("patsy","493-2928") ,
("lucille","205-2928") ,
("wendy","939-8282") ,
("penny","853-2492") ]
不理這貌似古怪的縮進(jìn),它就是一組二元組的 List 而已。話說對關(guān)聯(lián)列表最常見的操作就是按鍵索值,我們就寫個函數(shù)來實(shí)現(xiàn)它。
findKey :: (Eq k) => k -> [(k,v)] -> v
findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs
http://wiki.jikexueyuan.com/project/haskell-guide/images/legomap.png" alt="" />
簡潔漂亮。這個函數(shù)取一個鍵和 List 做參數(shù),過濾這一 List 僅保留鍵匹配的項(xiàng),并返回首個鍵值對。但若該關(guān)聯(lián)列表中不存在這個鍵那會怎樣? 哼,那就會在試圖從空 List 中取 head 時引發(fā)一個運(yùn)行時錯誤。無論如何也不能讓程序就這么輕易地崩潰吧,所以就應(yīng)該用 Maybe 型別。如果沒找到相應(yīng)的鍵,就返回 Nothing。而找到了就返回 Just something。而這 something 就是鍵對應(yīng)的值。
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key [] = Nothing
findKey key ((k,v):xs) =
if key == k then
Just v
else
findKey key xs
看這型別聲明,它取一個可判斷相等性的鍵和一個關(guān)聯(lián)列表做參數(shù),可能 (Maybe) 得到一個值。聽起來不錯.這便是個標(biāo)準(zhǔn)的處理 List 的遞歸函數(shù),邊界條件,分割 List,遞歸呼叫,都有了 -- 經(jīng)典的 fold 模式。
看看用 fold 怎樣實(shí)現(xiàn)吧。
findKey :: (Eq k) => k -> [(k,v)] -> Maybe v
findKey key = foldr (\(k,v) acc -> if key == k then Just v else acc) Nothing
*Note*: 通常,使用 ``fold`` 來替代類似的遞歸函數(shù)會更好些。用 ``fold`` 的程式碼讓人一目了然,而看明白遞歸則得多花點(diǎn)腦子。
ghci> findKey "penny" phoneBook
Just "853-2492"
ghci> findKey "betty" phoneBook
Just "555-2938"
ghci> findKey "wilma" phoneBook
Nothing
如魔咒般靈驗(yàn)! 只要我們有這姑娘的號碼就 Just 可以得到,否則就是 Nothing. 方才我們實(shí)現(xiàn)的函數(shù)便是 Data.List 模組的 lookup,如果要按鍵去尋找相應(yīng)的值,它就必須得遍歷整個 List,直到找到為止。而 Data.Map 模組提供了更高效的方式(通過樹實(shí)現(xiàn)),并提供了一組好用的函數(shù)。從現(xiàn)在開始,我們?nèi)拥絷P(guān)聯(lián)列表,改用map.由于Data.Map中的一些函數(shù)與Prelude和Data.List 模組存在命名沖突,所以我們使用 qualified import。import qualified Data.Map as Map 在程式碼中加上這句,并 load 到 ghci 中.繼續(xù)前進(jìn),看看 Data.Map 是如何的一座寶庫!
如下便是其中函數(shù)的一瞥:
fromList 取一個關(guān)聯(lián)列表,返回一個與之等價的 Map。
ghci> Map.fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
fromList [("betty","555-2938"),("bonnie","452-2928"),("lucille","205-2928")]
ghci> Map.fromList [(1,2),(3,4),(3,2),(5,5)]
fromList [(1,2),(3,2),(5,5)]
若其中存在重復(fù)的鍵,就將其忽略。如下即 fromList 的型別聲明。
Map.fromList :: (Ord k) => [(k,v)] -> Map.Map k v
這表示它取一組鍵值對的 List,并返回一個將 k 映射為 v 的 map。注意一下,當(dāng)使用普通的關(guān)聯(lián)列表時,只需要鍵的可判斷相等性就行了。而在這里,它還必須得是可排序的。這在 Data.Map 模組中是強(qiáng)制的。因?yàn)樗鼤凑漳稠樞驅(qū)⑵浣M織在一棵樹中.在處理鍵值對時,只要鍵的型別屬于 Ord 型別類,就應(yīng)該盡量使用Data.Map.empty 返回一個空 map.
ghci> Map.empty
fromList []
insert 取一個鍵,一個值和一個 map 做參數(shù),給這個 map 插入新的鍵值對,并返回一個新的 map。
ghci> Map.empty
fromList []
ghci> Map.insert 3 100 Map.empty
fromList [(3,100)]
ghci> Map.insert 5 600 (Map.insert 4 200 ( Map.insert 3 100 Map.empty))
fromList [(3,100),(4,200),(5,600)]
ghci> Map.insert 5 600 . Map.insert 4 200 . Map.insert 3 100 $ Map.empty
fromList [(3,100),(4,200),(5,600)]
通過 empty,insert 與 fold,我們可以編寫出自己的 fromList。
fromList' :: (Ord k) => [(k,v)] -> Map.Map k v
fromList' = foldr (\(k,v) acc -> Map.insert k v acc) Map.empty
簡潔明了的 fold! 從一個空的 map 開始,然后從右折疊,隨著遍歷不斷地往 map 中插入新的鍵值對.
null 檢查一個 map 是否為空.
ghci> Map.null Map.empty
True
ghci> Map.null $ Map.fromList [(2,3),(5,5)]
False
size 返回一個 map 的大小。
ghci> Map.size Map.empty
0
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5
singleton 取一個鍵值對做參數(shù),并返回一個只含有一個映射的 map.
ghci> Map.singleton 3 9
fromList [(3,9)]
ghci> Map.insert 5 9 $ Map.singleton 3 9
fromList [(3,9),(5,9)]
lookup 與 Data.List 的 lookup 很像,只是它的作用對象是 map,如果它找到鍵對應(yīng)的值。就返回 Just something,否則返回 Nothing。
member 是個判斷函數(shù),它取一個鍵與 map 做參數(shù),并返回該鍵是否存在于該 map。
ghci> Map.member 3 $ Map.fromList [(3,6),(4,3),(6,9)]
True
ghci> Map.member 3 $ Map.fromList [(2,5),(4,5)]
False
map 與 filter 與其對應(yīng)的 List 版本很相似:
ghci> Map.map (*100) $ Map.fromList [(1,1),(2,4),(3,9)]
fromList [(1,100),(2,400),(3,900)]
ghci> Map.filter isUpper $ Map.fromList [(1,'a'),(2,'A'),(3,'b'),(4,'B')]
fromList [(2,'A'),(4,'B')]
toList 是 fromList 的反函數(shù)。
ghci> Map.toList . Map.insert 9 2 $ Map.singleton 4 3
[(4,3),(9,2)]
keys 與 elems 各自返回一組由鍵或值組成的 List,keys 與 map fst . Map.toList 等價,elems 與 map snd . Map.toList等價. fromListWith 是個很酷的小函數(shù),它與 fromList 很像,只是它不會直接忽略掉重復(fù)鍵,而是交給一個函數(shù)來處理它們。假設(shè)一個姑娘可以有多個號碼,而我們有個像這樣的關(guān)聯(lián)列表:
phoneBook =
[("betty","555-2938")
,("betty","342-2492")
,("bonnie","452-2928")
,("patsy","493-2928")
,("patsy","943-2929")
,("patsy","827-9162")
,("lucille","205-2928")
,("wendy","939-8282")
,("penny","853-2492")
,("penny","555-2111")
]
如果用 fromList 來生成 map,我們會丟掉許多號碼! 如下才是正確的做法:
phoneBookToMap :: (Ord k) => [(k, String)] -> Map.Map k String
phoneBookToMap xs = Map.fromListWith (\number1 number2 -> number1 ++ ", " ++ number2) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
"827-9162, 943-2929, 493-2928"
ghci> Map.lookup "wendy" $ phoneBookToMap phoneBook
"939-8282"
ghci> Map.lookup "betty" $ phoneBookToMap phoneBook
"342-2492,555-2938"
一旦出現(xiàn)重復(fù)鍵,這個函數(shù)會將不同的值組在一起,同樣,也可以預(yù)設(shè)地將每個值放到一個單元素的 List 中,再用 ++ 將他們都連接在一起。
phoneBookToMap :: (Ord k) => [(k,a)] -> Map.Map k [a]
phoneBookToMap xs = Map.fromListWith (++) $ map (\(k,v) -> (k,[v])) xs
ghci> Map.lookup "patsy" $ phoneBookToMap phoneBook
["827-9162","943-2929","493-2928"]
很簡潔! 它還有別的玩法,例如在遇到重復(fù)元素時,單選最大的那個值.
ghci> Map.fromListWith max [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,100),(3,29),(4,22)]
或是將相同鍵的值都加在一起.
ghci> Map.fromListWith (+) [(2,3),(2,5),(2,100),(3,29),(3,22),(3,11),(4,22),(4,15)]
fromList [(2,108),(3,62),(4,37)]
insertWith 之于 insert,恰如 fromListWith 之于 fromList。它會將一個鍵值對插入一個 map 之中,而該 map 若已經(jīng)包含這個鍵,就問問這個函數(shù)該怎么辦。
ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)]
fromList [(3,104),(5,103),(6,339)]
Data.Map 里面還有不少函數(shù),[http://www.haskell.org/ghc/docs/latest/html/libraries/containers/Data-Map.html 這個文檔]中的列表就很全了.
http://wiki.jikexueyuan.com/project/haskell-guide/images/legosets.png" alt="" />
Data.Set 模組提供了對數(shù)學(xué)中集合的處理。集合既像 List 也像 Map: 它里面的每個元素都是唯一的,且內(nèi)部的數(shù)據(jù)由一棵樹來組織(這和 Data.Map 模組的 map 很像),必須得是可排序的。同樣是插入,刪除,判斷從屬關(guān)系之類的操作,使用集合要比 List 快得多。對一個集合而言,最常見的操作莫過于并集,判斷從屬或是將集合轉(zhuǎn)為 List.
由于 Data.Set 模組與 Prelude 模組和 Data.Li