第七章 Haskell模塊

2022-08-08 14:24 更新
  • 裝載模塊
  • Data.List
  • Data.Char
  • Data.Map
  • Data.Set
  • 建立自己的模塊

裝載模塊

haskell中的模塊是含有一組相關(guān)的函數(shù),類(lèi)型和類(lèi)型類(lèi)的組合。而haskell程序的本質(zhì)便是從主模塊中引用其它模塊并調(diào)用其中的函數(shù)來(lái)執(zhí)行操作。這樣可以把代碼分成多塊,只要一個(gè)模塊足夠的獨(dú)立,它里面的函數(shù)便可以被不同的程序反復(fù)重用。這就讓不同的代碼各司其職,提高了代碼的健壯性。

haskell的標(biāo)準(zhǔn)庫(kù)就是一組模塊,每個(gè)模塊都含有一組功能相近或相關(guān)的函數(shù)和類(lèi)型。有處理List的模塊,有處理并發(fā)的模塊,也有處理復(fù)數(shù)的模塊,等等。目前為止我們談及的所有函數(shù),類(lèi)型以及類(lèi)型類(lèi)都是Prelude模塊的一部分,它默認(rèn)自動(dòng)裝載。在本章,我們看一下幾個(gè)常用的模塊,在開(kāi)始瀏覽其中的函數(shù)之前,我們先得知道如何裝載模塊.

在haskell中,裝載模塊的語(yǔ)法為import,這必須得在函數(shù)的定義之前,所以一般都是將它置于代碼的頂部。無(wú)疑,一段代碼中可以裝載很多模塊,只要將import語(yǔ)句分行寫(xiě)開(kāi)即可。裝載Data.List試下,它里面有很多實(shí)用的List處理函數(shù).

執(zhí)行import Data.List,這樣一來(lái)Data.List中包含的所有函數(shù)就都進(jìn)入了全局命名空間。也就是說(shuō),你可以在代碼的任意位置調(diào)用這些函數(shù).Data.List模塊中有個(gè)nub函數(shù),它可以篩掉一個(gè)List中的所有重復(fù)元素。用點(diǎn)號(hào)將lengthnub組合:length . nub,即可得到一個(gè)與(\xs -> length (nub xs))等價(jià)的函數(shù)。

import Data.List   

numUniques :: (Eq a) => [a] -> Int   
numUniques = length . nub

你也可以在GHCi中裝載模塊,若要調(diào)用Data.List中的函數(shù),就這樣:

ghci> :m Data.List

若要在GHci中裝載多個(gè)模塊,不必多次:m命令,一下就可以全部搞定:

ghci> :m Data.List Data.Map Data.Set

而你的程序中若已經(jīng)有包含的代碼,就不必再用:m了.

如果你只用得到某模塊的兩個(gè)函數(shù),大可僅包含它倆。若僅裝載Data.List模塊nubsort,就這樣:

import Data.List (nub,sort)

也可以只包含除去某函數(shù)之外的其它函數(shù),這在避免多個(gè)模塊中函數(shù)的命名沖突很有用。假設(shè)我們的代碼中已經(jīng)有了一個(gè)叫做nub的函數(shù),而裝入Data.List模塊時(shí)就要把它里面的nub除掉.

import Data.List hiding (nub)

避免命名沖突還有個(gè)方法,便是qualified import,Data.Map模塊提供一了一個(gè)按鍵索值的數(shù)據(jù)結(jié)構(gòu),它里面有幾個(gè)和Prelude模塊重名的函數(shù)。如filternull,裝入Data.Map模塊之后再調(diào)用filter,haskell就不知道它究竟是哪個(gè)函數(shù)。如下便是解決的方法:

import qualified Data.Map

這樣一來(lái),再調(diào)用Data.Map中的filter函數(shù),就必須得Data.Map.filter,而filter依然是為我們熟悉喜愛(ài)的樣子。但是要在每個(gè)函數(shù)前面都加個(gè)Data.Map實(shí)在是太煩人了! 那就給它起個(gè)別名,讓它短些:

import qualified Data.Map as M

好,再調(diào)用Data.Map模塊的filter函數(shù)的話僅需M.filter就行了

要瀏覽所有的標(biāo)準(zhǔn)庫(kù)模塊,參考這個(gè)手冊(cè)。翻閱標(biāo)準(zhǔn)庫(kù)中的模塊和函數(shù)是提升個(gè)人haskell水平的重要途徑。你也可以各個(gè)模塊的源代碼,這對(duì)haskell的深入學(xué)習(xí)及掌握都是大有好處的.

檢索函數(shù)或搜尋函數(shù)位置就用Hoogle,相當(dāng)了不起的Haskell搜索引擎! 你可以用函數(shù)名,模塊名甚至類(lèi)型聲明來(lái)作為檢索的條件.

Data.List

顯而易見(jiàn),Data.List是關(guān)于List操作的模塊,它提供了一組非常有用的List處理函數(shù)。在前面我們已經(jīng)見(jiàn)過(guò)了其中的幾個(gè)函數(shù)(如map和filter),這是Prelude模塊出于方便起見(jiàn),導(dǎo)出了幾個(gè)Data.List里的函數(shù)。因?yàn)檫@幾個(gè)函數(shù)是直接引用自Data.List,所以就無(wú)需使用qulified import。在下面,我們來(lái)看看幾個(gè)以前沒(méi)見(jiàn)過(guò)的函數(shù):

intersperse取一個(gè)元素與List作參數(shù),并將該元素置于List中每對(duì)元素的中間。如下是個(gè)例子:

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取兩個(gè)List作參數(shù)。它會(huì)將第一個(gè)List交叉插入第二個(gè)List中間,并返回一個(gè)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看作是個(gè)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"]

假如有兩個(gè)多項(xiàng)式3x2+ 5x + 9,_10x3 + 9_和8x3 + 5x2 + x - 1,將其相加,我們可以列三個(gè)List:[0,3,5,9],[10,0,0,9][8,5,1,-1]來(lái)表示。再用如下的方法取得結(jié)果.

ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]   
[18,8,6,17]

使用transpose處理這三個(gè)List之后,三次冪就倒了第一行,二次冪到了第二行,以此類(lèi)推。在用sum函數(shù)將其映射,即可得到正確的結(jié)果。

foldl'和foldl1'是它們各自惰性實(shí)現(xiàn)的嚴(yán)格版本。在用fold處理較大的List時(shí),經(jīng)常會(huì)遇到堆棧溢出的問(wèn)題。而這罪魁禍?zhǔn)拙褪莊old的惰性: 在執(zhí)行fold時(shí),累加器的值并不會(huì)被立即更新,而是做一個(gè)"在必要時(shí)會(huì)取得所需的結(jié)果"的承諾。每過(guò)一遍累加器,這一行為就重復(fù)一次。而所有的這堆"承諾"最終就會(huì)塞滿你的堆棧。嚴(yán)格的fold就不會(huì)有這一問(wèn)題,它們不會(huì)作"承諾",而是直接計(jì)算中間值的結(jié)果并繼續(xù)執(zhí)行下去。如果用惰性fold時(shí)經(jīng)常遇到溢出錯(cuò)誤,就應(yīng)換用它們的嚴(yán)格版。

concat把一組List連接為一個(gè)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)于移除一級(jí)嵌套。若要徹底地連接其中的元素,你得concat它兩次才行.

concatMap函數(shù)與map一個(gè)List之后再concat它等價(jià).

ghci> concatMap (replicate 4) [1..3]   
[1,1,1,1,2,2,2,2,3,3,3,3]

and取一組布爾值List作參數(shù)。只有其中的值全為T(mén)rue的情況下才會(huì)返回True。

ghci> and $ map (>4) [5,6,7,8]   
True   
ghci> and $ map (==4) [4,4,4,3,4]   
False

or與and相似,一組布爾值List中若存在一個(gè)True它就返回True.

ghci> or $ map (==4) [2,3,4,5,6,1]   
True   
ghci> or $ map (>4) [1,2,3]   
False

any和all取一個(gè)限制條件和一組布爾值List作參數(shù),檢查是否該List的某個(gè)元素或每個(gè)元素都符合該條件。通常較map一個(gè)List到and或or而言,使用any或all會(huì)更多些。

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取一個(gè)函數(shù)和一個(gè)值作參數(shù)。它會(huì)用該值去調(diào)用該函數(shù)并用所得的結(jié)果再次調(diào)用該函數(shù),產(chǎn)生一個(gè)無(wú)限的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取一個(gè)List和數(shù)值作參數(shù),將該List在特定的位置斷開(kāi)。返回一個(gè)包含兩個(gè)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í)用。它從一個(gè)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來(lái)過(guò)濾map (^3) [1..]所得結(jié)果中所有小于1000的數(shù)是不行的。因?yàn)閷?duì)無(wú)限List執(zhí)行的filter永遠(yuǎn)都不會(huì)停止。你已經(jīng)知道了這個(gè)List是單增的,但haskell不知道。所以應(yīng)該這樣:

ghci> sum $ takeWhile (10000) $ map (^3) [1..]   
53361

(^3)處理一個(gè)無(wú)限List,而一旦出現(xiàn)了大于10000的元素這個(gè)List就被切斷了,sum到一起也就輕而易舉.

dropWhile與此相似,不過(guò)它是扔掉符合條件的元素。一旦限制條件返回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的首相表示股票價(jià)格,第二三四項(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)像,只是它返回兩個(gè)List。第一個(gè)List與同參數(shù)調(diào)用takeWhile所得的結(jié)果相同,第二個(gè)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時(shí)斷開(kāi)list,而break則是在條件首次為T(mén)rue時(shí)斷開(kāi)List。break pspan (not 。p)是等價(jià)的.

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返回的第二個(gè)List就會(huì)以第一個(gè)符合條件的元素開(kāi)頭。

sort可以排序一個(gè)List,因?yàn)橹挥心軌蜃鞅容^的元素才可以被排序,所以這一List的元素必須是Ord類(lèi)型類(lèi)的實(shí)例類(lèi)型。

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取一個(gè)List作參數(shù),并將其中相鄰并相等的元素各自歸類(lèi),組成一個(gè)個(gè)子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一個(gè)List之前給它排序就可以得到每個(gè)元素在該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與inittail相似,只是它們會(huì)遞歸地調(diào)用自身直到什么都不剩,看:

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)一個(gè)搜索子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)

首先,對(duì)搜索的List調(diào)用tails,然后遍歷每個(gè)List來(lái)檢查它是不是我們想要的.

ghci> "cat" `isInfixOf` "im a cat burglar"   
True   
ghci> "Cat" `isInfixOf` "im a cat burglar"   
False   
ghci> "cats" `isInfixOf` "im a cat burglar"   
False

由此我們便實(shí)現(xiàn)了一個(gè)類(lèi)似isIndexOf的函數(shù),isInfixOf從一個(gè)List中搜索一個(gè)子List,若該List包含子List,則返回True.

isPrefixOf與isSuffixOf分別檢查一個(gè)List是否以某子List開(kāi)頭或者結(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檢查一個(gè)List是否包含某元素.

partition取一個(gè)限制條件和List作參數(shù),返回兩個(gè)List,第一個(gè)List中包含所有符合條件的元素,而第二個(gè)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])

了解spanbreak的差異是很重要的.

ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"   
("BOB","sidneyMORGANeddy")

span和break會(huì)在遇到第一個(gè)符合或不符合條件的元素處斷開(kāi),而partition則會(huì)遍歷整個(gè)List。

find取一個(gè)List和限制條件作參數(shù),并返回首個(gè)符合該條件的元素,而這個(gè)元素是個(gè)Maybe值。在下章,我們將深入地探討相關(guān)的算法和數(shù)據(jù)結(jié)構(gòu),但在這里你只需了解Maybe值是Just something或Nothing就夠了。與一個(gè)List可以為空也可以包含多個(gè)元素相似,一個(gè)Maybe可以為空,也可以是單一元素。同樣與List類(lèi)似,一個(gè)Int型的List可以寫(xiě)作Int,Maybe有個(gè)Int型可以寫(xiě)作Maybe Int。先試一下find函數(shù)再說(shuō).

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的類(lèi)型,它的返回結(jié)果為Maybe a··,這與a的寫(xiě)法有點(diǎn)像,只是Maybe型的值只能為空或者單一元素,而List可以為空,一個(gè)元素,也可以是多個(gè)元素.

想想前面那段找股票的代碼,head (dropWhile (\(val,y,m,d) -> val < 1000) stock)。但head并不安全! 如果我們的股票沒(méi)漲過(guò)$1000會(huì)怎樣?dropWhile會(huì)返回一個(gè)空List,而對(duì)空List取head就會(huì)引發(fā)一個(gè)錯(cuò)誤。把它改成find (\(val,y,m,d) -> val > 1000) stock就安全多啦,若存在合適的結(jié)果就得到它,像Just (1001.4,2008,9,4),若不存在合適的元素(即我們的股票沒(méi)有漲到過(guò)$1000),就會(huì)得到一個(gè)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相似,只不過(guò)它返回的是List,就不需要Maybe了。因?yàn)椴淮嬖谟每誏ist就可以表示,這就與Nothing相似了.

ghci> ' ' `elemIndices` "Where are the spaces?"   
[5,9,13]

findIndex與find相似,但它返回的是可能存在的首個(gè)符合該條件元素的索引。findIndices會(huì)返回所有符合條件的索引.

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]

在前面,我們講過(guò)了zip和zipWidth,它們只能將兩個(gè)List組到一個(gè)二元組數(shù)或二參函數(shù)中,但若要組三個(gè)List該怎么辦? 好說(shuō)~有zip3,zip4,,,,和zipWith3,zipWidth4...直到7。這看起來(lái)像是個(gè)hack,但工作良好。連著組8個(gè)List的情況很少遇到。還有個(gè)聰明辦法可以組起無(wú)限多個(gè)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ǎng)度最短的那個(gè)為準(zhǔn).

在處理來(lái)自文件或其它地方的輸入時(shí),lines會(huì)非常有用。它取一個(gè)字符串作參數(shù)。并返回由其中的每一行組成的List.

ghci> lines "first line\nsecond line\nthird line"   
["first line","second line","third line"]

'\n'表示unix下的換行符,在haskell的字符中,反斜杠表示特殊字符.

unlines是lines的反函數(shù),它取一組字符串的List,并將其通過(guò)'\n'合并到一塊.

ghci> unlines ["first line","second line","third line"]   
"first line\nsecond line\nthird line\n"

words和unwords可以把一個(gè)字符串分為一組單詞或執(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,它可以將一個(gè)List中的重復(fù)元素全部篩掉,使該List的每個(gè)元素都如雪花般獨(dú)一無(wú)二,'nub'的含義就是'一小塊'或'一部分',用在這里覺(jué)得很古怪。我覺(jué)得,在函數(shù)的命名上應(yīng)該用更確切的詞語(yǔ),而避免使用老掉牙的過(guò)時(shí)詞匯.

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取一個(gè)元素和List作參數(shù),會(huì)刪掉該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的差集操作,這與集合的差集很相似,它會(huì)除掉左邊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與集合的并集也是很相似,它返回兩個(gè)List的并集,即遍歷第二個(gè)List若存在某元素不屬于第一個(gè)List,則追加到第一個(gè)List。看,第二個(gè)List中的重復(fù)元素就都沒(méi)了!

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)于集合的交集。它返回兩個(gè)List的相同部分.

ghci> [1..7] `intersect` [5..10]   
[5,6,7]

insert可以將一個(gè)元素插入一個(gè)可排序的List,并將其置于首個(gè)大于它的元素之前,如果使用insert來(lái)給一個(gè)排過(guò)序的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和replace之類(lèi)的函數(shù)有個(gè)共同點(diǎn)。那就是它們的參數(shù)中都有個(gè)Int值,我覺(jué)得使用Intergal或Num類(lèi)型類(lèi)會(huì)更好,但出于歷史原因,修改這些會(huì)破壞掉許多既有的代碼。在Data.List中包含了更通用的替代版,如:genericLength,genericTake,genericDrop,genericSplitAt,genericIndex 和 genericReplicate。length的類(lèi)型聲明為length :: [a] -> Int,而我們?nèi)粢襁@樣求它的平均值,let xs = [1..6] in sum xs / length xs,就會(huì)得到一個(gè)類(lèi)型錯(cuò)誤,因?yàn)?code>/運(yùn)算符不能對(duì)Int型使用! 而genericLength的類(lèi)型聲明則為genericLength :: (Num a) => [b] -> a,Num既可以是整數(shù)又可以是浮點(diǎn)數(shù),let xs = [1..6] in sum xs / genericLength xs這樣再求平均數(shù)就不會(huì)有問(wèn)題了.

nub,delete,union,intsectgroup函數(shù)也有各自的通用替代版nubBy,deleteBy,unionBy,intersectBygroupBy,它們的區(qū)別就是前一組函數(shù)使用(==)來(lái)測(cè)試是否相等,而帶By的那組則取一個(gè)函數(shù)作參數(shù)來(lái)判定相等性,group就與groupBy (==)等價(jià).

假如有個(gè)記錄某函數(shù)在每秒的值的List,而我們要按照它小于零或者大于零的交界處將其分為一組子List。如果用group,它只能將相鄰并相等的元素組到一起,而在這里我們的標(biāo)準(zhǔn)是它是否為負(fù)數(shù)。groupBy登場(chǎng)! 它取一個(gè)含兩個(gè)參數(shù)的函數(shù)作為參數(shù)來(lái)判定相等性.

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]]

這樣一來(lái)我們就可以很清楚地看出哪部分是正數(shù),哪部分是負(fù)數(shù),這個(gè)判斷相等性的函數(shù)會(huì)在兩個(gè)元素同時(shí)大于零或同時(shí)小于零時(shí)返回True。也可以寫(xiě)作\x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0)。但我覺(jué)得第一個(gè)寫(xiě)法的可讀性更高。Data.Function中還有個(gè)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)基本等價(jià)。on與帶By的函數(shù)在一起會(huì)非常好用,你可以這樣寫(xiě):

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]]

可讀性很高! 你可以大聲念出來(lái): 按照元素是否大于零,給它分類(lèi)!

同樣,sort,insert,maximummin都有各自的通用版本。如groupBy類(lèi)似,sortBy,insertBy,maximumBy和minimumBy都取一個(gè)函數(shù)來(lái)比較兩個(gè)元素的大小。像sortBy的類(lèi)型聲明為:sortBy :: (a -> a -> Ordering) -> [a] -> [a]。前面提過(guò),Ordering類(lèi)型可以有三個(gè)值,LT,EQGT。compare取兩個(gè)Ord類(lèi)型類(lèi)的元素作參數(shù),所以sortsortBy compare等價(jià).

List是可以比較大小的,且比較的依據(jù)就是其中元素的大小。如果按照其子List的長(zhǎng)度為標(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]]

太絕了!compareonlength,乖乖,這簡(jiǎn)直就是英文! 如果你搞不清楚on在這里的原理,就可以認(rèn)為它與\x y -> length xcomparelength y等價(jià)。通常,與帶By的函數(shù)打交道時(shí),若要判斷相等性,則(==)onsomething。若要判定大小,則compareonsomething.

Data.Char

如其名,Data.Char模塊包含了一組用于處理字符的函數(shù)。由于字符串的本質(zhì)就是一組字符的List,所以往往會(huì)在filter或是map字符串時(shí)用到它.

Data.Char模塊中含有一系列用于判定字符范圍的函數(shù),如下:

isControl判斷一個(gè)字符是否是控制字符。

isSpace判斷一個(gè)字符是否是空格字符,包括空格,tab,換行符等.

isLower判斷一個(gè)字符是否為小寫(xiě).

isUper判斷一個(gè)字符是否為大寫(xiě)。

isAlpha判斷一個(gè)字符是否為字母.

isAlphaNum判斷一個(gè)字符是否為字母或數(shù)字.

isPrint判斷一個(gè)字符是否是可打印的.

isDigit判斷一個(gè)字符是否為數(shù)字.

isOctDigit判斷一個(gè)字符是否為八進(jìn)制數(shù)字.

isHexDigit判斷一個(gè)字符是否為十六進(jìn)制數(shù)字.

isLetter判斷一個(gè)字符是否為字母.

isMark判斷是否為unicode注音字符,你如果是法國(guó)人就會(huì)經(jīng)常用到的.

isNumber判斷一個(gè)字符是否為數(shù)字.

isPunctuation判斷一個(gè)字符是否為標(biāo)點(diǎn)符號(hào).

isSymbol判斷一個(gè)字符是否為貨幣符號(hào).

isSeperater判斷一個(gè)字符是否為unicode空格或分隔符.

isAscii判斷一個(gè)字符是否在unicode字母表的前128位。

isLatin1判斷一個(gè)字符是否在unicode字母表的前256位.

isAsciiUpper判斷一個(gè)字符是否為大寫(xiě)的ascii字符.

isAsciiLower判斷一個(gè)字符是否為小寫(xiě)的ascii字符.

以上所有判斷函數(shù)的類(lèi)型聲明皆為Char -> Bool,用到它們的絕大多數(shù)情況都無(wú)非就是過(guò)濾字符串或類(lèi)似操作。假設(shè)我們?cè)趯?xiě)個(gè)程序,它需要一個(gè)由字符和數(shù)字組成的用戶名。要實(shí)現(xiàn)對(duì)用戶名的檢驗(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ù)取一個(gè)判斷函數(shù)和一個(gè)List做參數(shù),若該List的所有元素都符合條件,就返回True.

也可以使用isSpace來(lái)實(shí)現(xiàn)Data.Listwords函數(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,不錯(cuò),有點(diǎn)words的樣子了。只是還有空格在里面,恩,該怎么辦? 我知道,用filter濾掉它們!

啊哈.

Data.Char中也含有與Ordering相似的類(lèi)型。Ordering可以有兩個(gè)值,LT,GTEQ。這就是個(gè)枚舉,它表示了兩個(gè)元素作比較可能的結(jié)果.GeneralCategory類(lèi)型也是個(gè)枚舉,它表示了一個(gè)字符可能所在的分類(lèi)。而得到一個(gè)字符所在分類(lèi)的主要方法就是使用generalCategory函數(shù).它的類(lèi)型為:generalCategory :: Char -> GeneralCategory。那31個(gè)分類(lèi)就不在此一一列出了,試下這個(gè)函數(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類(lèi)型是Eq類(lèi)型類(lèi)的一部分,使用類(lèi)似generalCategory c == Space的代碼也是可以的.

toUpper將一個(gè)字符轉(zhuǎn)為大寫(xiě)字母,若該字符不是小寫(xiě)字母,就按原值返回.

toLower將一個(gè)字符轉(zhuǎn)為小寫(xiě)字母,若該字符不是大寫(xiě)字母,就按原值返回.

toTitle將一個(gè)字符轉(zhuǎn)為title-case,對(duì)大多數(shù)字符而言,title-case就是大寫(xiě).

digitToInt將一個(gè)字符轉(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]

intToDigitdigitToInt的反函數(shù)。它取一個(gè)015Int值作參數(shù),并返回一個(gè)小寫(xiě)的字符.

ghci> intToDigit 15   
'f'   
ghci> intToDigit 5   
'5'

ord與char函數(shù)可以將字符與其對(duì)應(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]

兩個(gè)字符的ord值之差就是它們?cè)趗nicode字符表上的距離.

_Caesar ciphar_是加密的基礎(chǔ)算法,它將消息中的每個(gè)字符都按照特定的字母表進(jìn)行替換。它的實(shí)現(xiàn)非常簡(jiǎn)單,我們這里就先不管字母表了.

encode :: Int -> String -> String   
encode shift msg =  
  let ords = map ord msg   
  shifted = map (+ shift) ords   
  in map chr shifted

先將一個(gè)字符串轉(zhuǎn)為一組數(shù)字,然后給它加上某數(shù),再轉(zhuǎn)回去。如果你是標(biāo)準(zhǔn)的組合牛仔,大可將函數(shù)寫(xiě)為: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&"

不錯(cuò)。再簡(jiǎn)單地將它轉(zhuǎn)成一組數(shù)字,減去某數(shù)后再轉(zhuǎn)回來(lái)就是解密了.

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"

Data.Map

關(guān)聯(lián)列表(也叫做字典)是按照鍵值對(duì)排列而沒(méi)有特定順序的一種List。例如,我們用關(guān)聯(lián)列表儲(chǔ)存電話號(hào)碼,號(hào)碼就是值,人名就是鍵。我們并不關(guān)心它們的存儲(chǔ)順序,只要能按人名得到正確的號(hào)碼就好.在haskell中表示關(guān)聯(lián)列表的最簡(jiǎn)單方法就是弄一個(gè)二元組的List,而這二元組就首項(xiàng)為鍵,后項(xiàng)為值。如下便是個(gè)表示電話號(hào)碼的關(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而已。話說(shuō)對(duì)關(guān)聯(lián)列表最常見(jiàn)的操作就是按鍵索值,我們就寫(xiě)個(gè)函數(shù)來(lái)實(shí)現(xiàn)它。

findKey :: (Eq k) => k -> [(k,v)] -> v  
findKey key xs = snd . head . filter (\(k,v) -> key == k) $ xs

簡(jiǎn)潔漂亮。這個(gè)函數(shù)取一個(gè)鍵和List做參數(shù),過(guò)濾這一List僅保留鍵匹配的項(xiàng),并返回首個(gè)鍵值對(duì)。但若該關(guān)聯(lián)列表中不存在這個(gè)鍵那會(huì)怎樣? 哼,那就會(huì)在試圖從空List中取head時(shí)引發(fā)一個(gè)運(yùn)行時(shí)錯(cuò)誤。無(wú)論如何也不能讓程序就這么輕易地崩潰吧,所以就應(yīng)該用Maybe類(lèi)型。如果沒(méi)找到相應(yīng)的鍵,就返回Nothing。而找到了就返回Just something。而這something就是鍵對(duì)應(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

看這類(lèi)型聲明,它取一個(gè)可判斷相等性的鍵和一個(gè)關(guān)聯(lián)列表做參數(shù),可能(Maybe)得到一個(gè)值。聽(tīng)起來(lái)不錯(cuò).這便是個(gè)標(biāo)準(zhǔn)的處理List的遞歸函數(shù),邊界條件,分割List,遞歸調(diào)用,都有了 -- 經(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來(lái)替代類(lèi)似的遞歸函數(shù)會(huì)更好些。用fold的代碼讓人一目了然,而看明白遞歸則得多花點(diǎn)腦子。

ghci> findKey "penny" phoneBook  
Just "853-2492"  
ghci> findKey "betty" phoneBook  
Just "555-2938"  
ghci> findKey "wilma" phoneBook  
Nothing

如魔咒般靈驗(yàn)! 只要我們有這姑娘的號(hào)碼就Just可以得到,否則就是Nothing.方才我們實(shí)現(xiàn)的函數(shù)便是Data.List模塊的lookup,如果要按鍵去尋找相應(yīng)的值,它就必須得遍歷整個(gè)List,直到找到為止。而Data.Map模塊提供了更高效的方式(通過(guò)樹(shù)實(shí)現(xiàn)),并提供了一組好用的函數(shù)。從現(xiàn)在開(kāi)始,我們?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是如何的一座寶庫(kù)!

如下便是其中函數(shù)的一瞥:

fromList取一個(gè)關(guān)聯(lián)列表,返回一個(gè)與之等價(jià)的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的類(lèi)型聲明。

Map.fromList :: (Ord k) => [(k,v)] -> Map.Map k v

這表示它取一組鍵值對(duì)的List,并返回一個(gè)將k映射為v的map。注意一下,當(dāng)使用普通的關(guān)聯(lián)列表時(shí),只需要鍵的可判斷相等性就行了。而在這里,它還必須得是可排序的。這在Data.Map模塊中是強(qiáng)制的。因?yàn)樗鼤?huì)按照某順序?qū)⑵浣M織在一棵樹(shù)中.在處理鍵值對(duì)時(shí),只要鍵的類(lèi)型屬于Ord類(lèi)型類(lèi),就應(yīng)該盡量使用Data.Map.empty返回一個(gè)空map.

ghci> Map.empty  
fromList []

insert取一個(gè)鍵,一個(gè)值和一個(gè)map做參數(shù),給這個(gè)map插入新的鍵值對(duì),并返回一個(gè)新的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)]

通過(guò)empty,insertfold,我們可以編寫(xiě)出自己的fromList。

fromList' :: (Ord k) => [(k,v)] -> Map.Map k v  
fromList' = foldr (\(k,v) acc -> Map.insert k v acc) Map.empty

多直白的fold! 從一個(gè)空的map開(kāi)始,然后從右折疊,隨著遍歷不斷地往map中插入新的鍵值對(duì).

null檢查一個(gè)map是否為空.

ghci> Map.null Map.empty  
True  
ghci> Map.null $ Map.fromList [(2,3),(5,5)]  
False

size返回一個(gè)map的大小。

ghci> Map.size Map.empty  
0  
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]  
5

singleton取一個(gè)鍵值對(duì)做參數(shù),并返回一個(gè)只含有一個(gè)映射的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.Listlookup很像,只是它的作用對(duì)象是map,如果它找到鍵對(duì)應(yīng)的值。就返回Just something,否則返回Nothing。

member是個(gè)判斷函數(shù),它取一個(gè)鍵與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與其對(duì)應(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等價(jià),elemsmap snd 。Map.toList等價(jià).fromListWith是個(gè)很酷的小函數(shù),它與fromList很像,只是它不會(huì)直接忽略掉重復(fù)鍵,而是交給一個(gè)函數(shù)來(lái)處理它們。假設(shè)一個(gè)姑娘可以有多個(gè)號(hào)碼,而我們有個(gè)像這樣的關(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來(lái)生成map,我們會(huì)丟掉許多號(hào)碼! 如下才是正確的做法:

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ù)鍵,這個(gè)函數(shù)會(huì)將不同的值組在一起,同樣,也可以默認(rèn)地將每個(gè)值放到一個(gè)單元素的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"]

很簡(jiǎn)潔! 它還有別的玩法,例如在遇到重復(fù)元素時(shí),單選最大的那個(gè)值.

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。它會(huì)將一個(gè)鍵值對(duì)插入一個(gè)map之中,而該map若已經(jīng)包含這個(gè)鍵,就問(wèn)問(wèn)這個(gè)函數(shù)該怎么辦。

ghci> Map.insertWith (+) 3 100 $ Map.fromList [(3,4),(5,103),(6,339)]  
fromList [(3,104),(5,103),(6,339)]

Data.Map里面還有不少函數(shù),這個(gè)文檔中的列表就很全了.

Data.Set

spaces_-LjUmp_4rLaEvLYs4D0h_uploads_git-blob-e5797fa84b870f824d30d28ac2f8518df4a65f0c_legosets

Data.Set 模塊提供了對(duì)數(shù)學(xué)中集合的處理。集合既像 List 也像 Map: 它里面的每個(gè)元素都是唯一的,且內(nèi)部的數(shù)據(jù)由一棵樹(shù)來(lái)組織(這和 Data.Map 模塊的 map 很像),必須得是可排序的。同樣是插入,刪除,判斷從屬關(guān)系之類(lèi)的操作,使用集合要比 List 快得多。對(duì)一個(gè)集合而言,最常見(jiàn)的操作莫過(guò)于并集,判斷從屬或是將集合轉(zhuǎn)為 List.

由于 Data.Set 模塊與 Prelude 模塊和 Data.List 模塊中存在大量的命名沖突,所以我們使用 qualified import

import 語(yǔ)句至于代碼之中:

import qualified Data.Set as Set

然后在 ghci 中裝載

假定我們有兩個(gè)字串,要找出同時(shí)存在于兩個(gè)字串的字符

text1 = "I just had an anime dream. Anime... Reality... Are they so different?"  
text2 = "The old man left his garbage can out and now his trash is all over my lawn!"

fromList 函數(shù)同你想的一樣,它取一個(gè) List 作參數(shù)并將其轉(zhuǎn)為一個(gè)集合

ghci> let set1 = Set.fromList text1  
ghci> let set2 = Set.fromList text2  
ghci> set1  
fromList " .?AIRadefhijlmnorstuy"  
ghci> set2  
fromList " !Tabcdefghilmnorstuvwy"

如你所見(jiàn),所有的元素都被排了序。而且每個(gè)元素都是唯一的?,F(xiàn)在我們?nèi)∷慕患纯此鼈児餐脑?

ghci> Set.intersection set1 set2  
fromList " adefhilmnorstuy"

使用 difference 函數(shù)可以得到存在于第一個(gè)集合但不在第二個(gè)集合的元素

ghci> Set.difference set1 set2  
fromList ".?AIRj"  
ghci> Set.difference set2 set1  
fromList "!Tbcgvw"

也可以使用 union 得到兩個(gè)集合的并集

ghci> Set.union set1 set2  
fromList " !.?AIRTabcdefghijlmnorstuvwy"

nullsize,member,empty,singletoninsert,delete 這幾個(gè)函數(shù)就跟你想的差不多啦

ghci> Set.null Set.empty  
True  
ghci> Set.null $ Set.fromList [3,4,5,5,4,3]  
False  
ghci> Set.size $ Set.fromList [3,4,5,3,4,5]  
3  
ghci> Set.singleton 9  
fromList [9]  
ghci> Set.insert 4 $ Set.fromList [9,3,8,1]  
fromList [1,3,4,8,9]  
ghci> Set.insert 8 $ Set.fromList [5..10]  
fromList [5,6,7,8,9,10]  
ghci> Set.delete 4 $ Set.fromList [3,4,5,4,3,4,5]  
fromList [3,5]

也可以判斷子集與真子集,如果集合 A 中的元素都屬于集合 B,那么 A 就是 B 的子集, 如果 A 中的元素都屬于 B 且 B 的元素比 A 多,那 A 就是 B 的真子集

ghci> Set.fromList [2,3,4] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]  
True  
ghci> Set.fromList [1,2,3,4,5] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]  
True  
ghci> Set.fromList [1,2,3,4,5] `Set.isProperSubsetOf` Set.fromList [1,2,3,4,5]  
False  
ghci> Set.fromList [2,3,4,8] `Set.isSubsetOf` Set.fromList [1,2,3,4,5]  
False

對(duì)集合也可以執(zhí)行 mapfilter:

ghci> Set.filter odd $ Set.fromList [3,4,5,6,7,2,3,4]  
fromList [3,5,7]  
ghci> Set.map (+1) $ Set.fromList [3,4,5,6,7,2,3,4]  
fromList [3,4,5,6,7,8]

集合有一常見(jiàn)用途,那就是先 fromList 刪掉重復(fù)元素后再 toList 轉(zhuǎn)回去。盡管 Data.List 模塊的 nub 函數(shù)完全可以完成這一工作,但在對(duì)付大 List 時(shí)則會(huì)明顯的力不從心。使用集合則會(huì)快很多,nub 函數(shù)只需 List 中的元素屬于 Eq 型別類(lèi)就行了,而若要使用集合,它必須得屬于 Ord 型別類(lèi)

ghci> let setNub xs = Set.toList $ Set.fromList xs  
ghci> setNub "HEY WHATS CRACKALACKIN"  
" ACEHIKLNRSTWY"  
ghci> nub "HEY WHATS CRACKALACKIN"  
"HEY WATSCRKLIN"

在處理較大的 List 時(shí),setNub 要比 nub 快,但也可以從中看出,nub 保留了 List 中元素的原有順序,而 setNub 不。

建立自己的模塊

我們已經(jīng)見(jiàn)識(shí)過(guò)了幾個(gè)很酷的模塊,但怎樣才能構(gòu)造自己的模塊呢? 幾乎所有的編程語(yǔ)言都允許你將代碼分成多個(gè)文件,Haskell 也不例外。在編程時(shí),將功能相近的函數(shù)和型別至于同一模塊中會(huì)是個(gè)很好的習(xí)慣。這樣一來(lái),你就可以輕松地一個(gè) import 來(lái)重用其中的函數(shù).

接下來(lái)我們將構(gòu)造一個(gè)由計(jì)算機(jī)幾何圖形體積和面積組成的模塊,先從新建一個(gè) Geometry.hs 的文件開(kāi)始.

在模塊的開(kāi)頭定義模塊的名稱(chēng),如果文件名叫做 Geometry.hs 那它的名字就得是 Geometry。在聲明出它含有的函數(shù)名之后就可以編寫(xiě)函數(shù)的實(shí)現(xiàn)啦,就這樣寫(xiě):

module Geometry  
( sphereVolume  
, sphereArea  
, cubeVolume  
, cubeArea  
, cuboidArea  
, cuboidVolume  
) where

如你所見(jiàn),我們提供了對(duì)球體,立方體和立方體的面積和體積的解法。繼續(xù)進(jìn)發(fā),定義函數(shù)體:

module Geometry  
( sphereVolume  
, sphereArea  
, cubeVolume  
, cubeArea  
, cuboidArea  
, cuboidVolume  
) where  

sphereVolume :: Float -> Float  
sphereVolume radius = (4.0 / 3.0) * pi * (radius ^ 3)  

sphereArea :: Float -> Float  
sphereArea radius = 4 * pi * (radius ^ 2)  

cubeVolume :: Float -> Float  
cubeVolume side = cuboidVolume side side side  

cubeArea :: Float -> Float  
cubeArea side = cuboidArea side side side  

cuboidVolume :: Float -> Float -> Float -> Float  
cuboidVolume a b c = rectangleArea a b * c  

cuboidArea :: Float -> Float -> Float -> Float  
cuboidArea a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2  

rectangleArea :: Float -> Float -> Float  
rectangleArea a b = a * b

spaces_-LjUmp_4rLaEvLYs4D0h_uploads_git-blob-c700a0bf15c12e31b56ad3139e8b56cf82962131_making_modules

標(biāo)準(zhǔn)的幾何公式。有幾個(gè)地方需要注意一下,由于立方體只是長(zhǎng)方體的特殊形式,所以在求它面積和體積的時(shí)候我們就將它當(dāng)作是邊長(zhǎng)相等的長(zhǎng)方體。在這里還定義了一個(gè) helper函數(shù),rectangleArea 它可以通過(guò)長(zhǎng)方體的兩條邊計(jì)算出長(zhǎng)方體的面積。它僅僅是簡(jiǎn)單的相乘而已,份量不大。但請(qǐng)注意我們可以在這一模塊中調(diào)用這個(gè)函數(shù),而它不會(huì)被導(dǎo)出! 因?yàn)槲覀冞@個(gè)模塊只與三維圖形打交道.

當(dāng)構(gòu)造一個(gè)模塊的時(shí)候,我們通常只會(huì)導(dǎo)出那些行為相近的函數(shù),而其內(nèi)部的實(shí)現(xiàn)則是隱蔽的。如果有人用到了 Geometry 模塊,就不需要關(guān)心它的內(nèi)部實(shí)現(xiàn)是如何。我們作為編寫(xiě)者,完全可以隨意修改這些函數(shù)甚至將其刪掉,沒(méi)有人會(huì)注意到里面的變動(dòng),因?yàn)槲覀儾⒉话阉鼈儗?dǎo)出.

要使用我們的模塊,只需:

import Geometry

Geometry.hs 文件至于用到它的進(jìn)程文件的同一目錄之下.

模塊也可以按照分層的結(jié)構(gòu)來(lái)組織,每個(gè)模塊都可以含有多個(gè)子模塊。而子模塊還可以有自己的子模塊。我們可以把 Geometry 分成三個(gè)子模塊,而一個(gè)模塊對(duì)應(yīng)各自的圖形對(duì)象.

首先,建立一個(gè) Geometry 文件夾,注意首字母要大寫(xiě),在里面新建三個(gè)文件

如下就是各個(gè)文件的內(nèi)容:

Sphere.hs

module Geometry.Sphere  
( volume  
, area  
) where  

volume :: Float -> Float  
volume radius = (4.0 / 3.0) * pi * (radius ^ 3)  

area :: Float -> Float  
area radius = 4 * pi * (radius ^ 2)

Cuboid.hs

module Geometry.Cuboid  
( volume  
, area  
) where  

volume :: Float -> Float -> Float -> Float  
volume a b c = rectangleArea a b * c  

area :: Float -> Float -> Float -> Float  
area a b c = rectangleArea a b * 2 + rectangleArea a c * 2 + rectangleArea c b * 2  

rectangleArea :: Float -> Float -> Float  
rectangleArea a b = a * b

Cube.hs

module Geometry.Cube  
( volume  
, area  
) where  

import qualified Geometry.Cuboid as Cuboid  

volume :: Float -> Float  
volume side = Cuboid.volume side side side  

area :: Float -> Float  
area side = Cuboid.area side side side

好的! 先是 Geometry.Sphere。注意,我們將它置于 Geometry 文件夾之中并將它的名字定為 Geometry.Sphere。對(duì) Cuboid 也是同樣,也注意下,在三個(gè)模塊中我們定義了許多名稱(chēng)相同的函數(shù),因?yàn)樗谀K不同,所以不會(huì)產(chǎn)生命名沖突。若要在 Geometry.Cube 使用 Geometry.Cuboid 中的函數(shù),就不能直接 import Geometry.Cuboid,而必須得 qualified import。因?yàn)樗鼈冎虚g的函數(shù)名完全相同.

import Geometry.Sphere

然后,調(diào)用 areavolume,就可以得到球體的面積和體積,而若要用到兩個(gè)或更多此類(lèi)模塊,就必須得 qualified import 來(lái)避免重名。所以就得這樣寫(xiě):

import qualified Geometry.Sphere as Sphere  
import qualified Geometry.Cuboid as Cuboid  
import qualified Geometry.Cube as Cube

然后就可以調(diào)用 Sphere.area,Sphere.volumeCuboid.area 了,而每個(gè)函數(shù)都只計(jì)算其對(duì)應(yīng)物體的面積和體積.

以后你若發(fā)現(xiàn)自己的代碼體積龐大且函數(shù)眾多,就應(yīng)該試著找找目的相近的函數(shù)能否裝入各自的模塊,也方便日后的重用.


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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)