[TOC]
## 裝載模塊

haskell中的模塊是含有一組相關的函數,類型和類型類的組合。而haskell程序的本質便是從主模塊中引用其它模塊并調用其中的函數來執行操作。這樣可以把代碼分成多塊,只要一個模塊足夠的獨立,它里面的函數便可以被不同的程序反復重用。這就讓不同的代碼各司其職,提高了代碼的健壯性。
haskell的標準庫就是一組模塊,每個模塊都含有一組功能相近或相關的函數和類型。有處理List的模塊,有處理并發的模塊,也有處理復數的模塊,等等。目前為止我們談及的所有函數,類型以及類型類都是Prelude模塊的一部分,它默認自動裝載。在本章,我們看一下幾個常用的模塊,在開始瀏覽其中的函數之前,我們先得知道如何裝載模塊.
在haskell中,裝載模塊的語法為_import_,這必須得在函數的定義之前,所以一般都是將它置于代碼的頂部。無疑,一段代碼中可以裝載很多模塊,只要將import語句分行寫開即可。裝載Data.List試下,它里面有很多實用的List處理函數.
執行`import Data.List`,這樣一來`Data.List`中包含的所有函數就都進入了全局命名空間。也就是說,你可以在代碼的任意位置調用這些函數.`Data.List`模塊中有個`nub`函數,它可以篩掉一個List中的所有重復元素。用點號將`length`和`nub`組合:`length . nub`,即可得到一個與`(\xs -> length (nub xs))`等價的函數。
~~~
import?Data.List???
numUniques?::?(Eq?a)?=>?[a]?->?Int???
numUniques?=?length?.?nub
~~~
你也可以在GHCi中裝載模塊,若要調用`Data.List`中的函數,就這樣:
~~~
ghci>?:m?Data.List
~~~
若要在GHci中裝載多個模塊,不必多次`:m`命令,一下就可以全部搞定:
~~~
ghci>?:m?Data.List?Data.Map?Data.Set
~~~
而你的程序中若已經有包含的代碼,就不必再用`:m`了.
如果你只用得到某模塊的兩個函數,大可僅包含它倆。若僅裝載`Data.List`模塊`nub`和`sort`,就這樣:
~~~
import?Data.List?(nub,sort)
~~~
也可以只包含除去某函數之外的其它函數,這在避免多個模塊中函數的命名沖突很有用。假設我們的代碼中已經有了一個叫做`nub`的函數,而裝入`Data.List`模塊時就要把它里面的nub除掉.
~~~
import?Data.List?hiding?(nub)
~~~
避免命名沖突還有個方法,便是`qualified import`,`Data.Map`模塊提供一了一個按鍵索值的數據結構,它里面有幾個和Prelude模塊重名的函數。如`filter`和`null`,裝入Data.Map模塊之后再調用filter,haskell就不知道它究竟是哪個函數。如下便是解決的方法:
~~~
import?qualified?Data.Map
~~~
這樣一來,再調用`Data.Map`中的`filter`函數,就必須得`Data.Map.filter`,而`filter`依然是為我們熟悉喜愛的樣子。但是要在每個函數前面都加`個Data.Map`實在是太煩人了! 那就給它起個別名,讓它短些:
~~~
import?qualified?Data.Map?as?M
~~~
好,再調用`Data.Map`模塊的`filter`函數的話僅需`M.filter`就行了
要瀏覽所有的標準庫模塊,參考這個手冊。翻閱標準庫中的模塊和函數是提升個人haskell水平的重要途徑。你也可以各個模塊的源代碼,這對haskell的深入學習及掌握都是大有好處的.
檢索函數或搜尋函數位置就用[Hoogle](http://www.haskell.org/hoogle/),相當了不起的Haskell搜索引擎! 你可以用函數名,模塊名甚至類型聲明來作為檢索的條件.
## Data.List
顯而易見,Data.List是關于List操作的模塊,它提供了一組非常有用的List處理函數。在前面我們已經見過了其中的幾個函數(如map和filter),這是Prelude模塊出于方便起見,導出了幾個Data.List里的函數。因為這幾個函數是直接引用自Data.List,所以就無需使用qulified import。在下面,我們來看看幾個以前沒見過的函數:
intersperse取一個元素與List作參數,并將該元素置于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作參數。它會將第一個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函數可以反轉一組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"]
~~~
假如有兩個多項式_3x2_+ 5x + 9,_10x3?+ 9_和_8x3?+ 5x2?+ x - 1_,將其相加,我們可以列三個List:`[0,3,5,9]`,`[10,0,0,9]`和`[8,5,1,-1]`來表示。再用如下的方法取得結果.
~~~
ghci>?map?sum?$?transpose?[[0,3,5,9],[10,0,0,9],[8,5,1,-1]]???
[18,8,6,17]
~~~

使用`transpose`處理這三個List之后,三次冪就倒了第一行,二次冪到了第二行,以此類推。在用sum函數將其映射,即可得到正確的結果。
foldl'和foldl1'是它們各自惰性實現的嚴格版本。在用fold處理較大的List時,經常會遇到堆棧溢出的問題。而這罪魁禍首就是fold的惰性: 在執行fold時,累加器的值并不會被立即更新,而是做一個"在必要時會取得所需的結果"的承諾。每過一遍累加器,這一行為就重復一次。而所有的這堆"承諾"最終就會塞滿你的堆棧。嚴格的fold就不會有這一問題,它們不會作"承諾",而是直接計算中間值的結果并繼續執行下去。如果用惰性fold時經常遇到溢出錯誤,就應換用它們的嚴格版。
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]
~~~
它相當于移除一級嵌套。若要徹底地連接其中的元素,你得`concat`它兩次才行.
concatMap函數與map一個List之后再concat它等價.
~~~
ghci>?concatMap?(replicate?4)?[1..3]???
[1,1,1,1,2,2,2,2,3,3,3,3]
~~~
and取一組布爾值List作參數。只有其中的值全為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作參數,檢查是否該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取一個函數和一個值作參數。它會用該值去調用該函數并用所得的結果再次調用該函數,產生一個無限的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和數值作參數,將該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這一函數十分的實用。它從一個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的數的和,用filter來過濾`map (^3) [1..]`所得結果中所有小于1000的數是不行的。因為對無限List執行的filter永遠都不會停止。你已經知道了這個List是單增的,但haskell不知道。所以應該這樣:
~~~
ghci>?sum?$?takeWhile?(10000)?$?map?(^3)?[1..]???
53361
~~~
用`(^3)`處理一個無限List,而一旦出現了大于10000的元素這個List就被切斷了,sum到一起也就輕而易舉.
dropWhile與此相似,不過它是扔掉符合條件的元素。一旦限制條件返回False,它就返回List的余下部分。方便實用!
~~~
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的首相表示股票價格,第二三四項分別表示年,月,日。我們想知道它是在哪天首次突破$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`有點像,只是它返回兩個List。第一個List與同參數調用takeWhile所得的結果相同,第二個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,因為只有能夠作比較的元素才可以被排序,所以這一List的元素必須是Ord類型類的實例類型。
~~~
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作參數,并將其中相鄰并相等的元素各自歸類,組成一個個子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中的出現次數。
~~~
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實現一個搜索子List的函數:
~~~
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來檢查它是不是我們想要的.
~~~
ghci>?"cat"?`isInfixOf`?"im?a?cat?burglar"???
True???
ghci>?"Cat"?`isInfixOf`?"im?a?cat?burglar"???
False???
ghci>?"cats"?`isInfixOf`?"im?a?cat?burglar"???
False
~~~
由此我們便實現了一個類似isIndexOf的函數,isInfixOf從一個List中搜索一個子List,若該List包含子List,則返回True.
isPrefixOf與isSuffixOf分別檢查一個List是否以某子List開頭或者結尾.
~~~
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作參數,返回兩個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和限制條件作參數,并返回首個符合該條件的元素,而這個元素是個Maybe值。在下章,我們將深入地探討相關的算法和數據結構,但在這里你只需了解Maybe值是Just something或Nothing就夠了。與一個List可以為空也可以包含多個元素相似,一個Maybe可以為空,也可以是單一元素。同樣與List類似,一個Int型的List可以寫作[Int](http://fleurer-lee.com/lyah/Int),Maybe有個Int型可以寫作Maybe Int。先試一下find函數再說.
~~~
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`的類型,它的返回結果為`Maybe a··,這與`[a](http://fleurer-lee.com/lyah/a)的寫法有點像,只是Maybe型的值只能為空或者單一元素,而List可以為空,一個元素,也可以是多個元素.
想想前面那段找股票的代碼,`head (dropWhile (\(val,y,m,d) -> val < 1000) stock)`。但head并不安全! 如果我們的股票沒漲過$1000會怎樣?`dropWhile`會返回一個空List,而對空List取head就會引發一個錯誤。把它改成`find (\(val,y,m,d) -> val > 1000) stock`就安全多啦,若存在合適的結果就得到它,像`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了。因為不存在用空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和zipWidth,它們只能將兩個List組到一個二元組數或二參函數中,但若要組三個List該怎么辦? 好說~有`zip3`,`zip4`,,,,和`zipWith3`,`zipWidth4`...直到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中長度最短的那個為準.
在處理來自文件或其它地方的輸入時,lines會非常有用。它取一個字符串作參數。并返回由其中的每一行組成的List.
~~~
ghci>?lines?"first?line\nsecond?line\nthird?line"???
["first?line","second?line","third?line"]
~~~
'\n'表示unix下的換行符,在haskell的字符中,反斜杠表示特殊字符.
unlines是lines的反函數,它取一組字符串的List,并將其通過'\n'合并到一塊.
~~~
ghci>?unlines?["first?line","second?line","third?line"]???
"first?line\nsecond?line\nthird?line\n"
~~~
words和unwords可以把一個字符串分為一組單詞或執行相反的操作,很有用.
~~~
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中的重復元素全部篩掉,使該List的每個元素都如雪花般獨一無二,'nub'的含義就是'一小塊'或'一部分',用在這里覺得很古怪。我覺得,在函數的命名上應該用更確切的詞語,而避免使用老掉牙的過時詞匯.
~~~
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作參數,會刪掉該List中首次出現的這一元素.
~~~
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中的重復元素就都沒了!
~~~
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相當于集合的交集。它返回兩個List的相同部分.
~~~
ghci>?[1..7]?`intersect`?[5..10]???
[5,6,7]
~~~
insert可以將一個元素插入一個可排序的List,并將其置于首個大于它的元素之前,如果使用insert來給一個排過序的List插入元素,返回的結果依然是排序的.
~~~
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之類的函數有個共同點。那就是它們的參數中都有個Int值,我覺得使用Intergal或Num類型類會更好,但出于歷史原因,修改這些會破壞掉許多既有的代碼。在Data.List中包含了更通用的替代版,如:genericLength,genericTake,genericDrop,genericSplitAt,genericIndex 和 genericReplicate。length的類型聲明為`length :: [a] -> Int`,而我們若要像這樣求它的平均值,`let xs = [1..6] in sum xs / length xs`,就會得到一個類型錯誤,因為`/`運算符不能對Int型使用! 而`genericLength`的類型聲明則`為genericLength :: (Num a) => [b] -> a`,Num既可以是整數又可以是浮點數,`let xs = [1..6] in sum xs / genericLength xs`這樣再求平均數就不會有問題了.
`nub`,`delete`,`union`,`intsect`和`group`函數也有各自的通用替代版`nubBy`,`deleteBy`,`unionBy`,`intersectBy`和`groupBy`,它們的區別就是前一組函數使用(==)來測試是否相等,而帶By的那組則取一個函數作參數來判定相等性,group就與groupBy (==)等價.
假如有個記錄某函數在每秒的值的List,而我們要按照它小于零或者大于零的交界處將其分為一組子List。如果用`group`,它只能將相鄰并相等的元素組到一起,而在這里我們的標準是它是否為負數。groupBy登場! 它取一個含兩個參數的函數作為參數來判定相等性.
~~~
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]]
~~~
這樣一來我們就可以很清楚地看出哪部分是正數,哪部分是負數,這個判斷相等性的函數會在兩個元素同時大于零或同時小于零時返回True。也可以寫作`\x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0)`。但我覺得第一個寫法的可讀性更高。Data.Function中還有個on函數可以讓它的表達更清晰,其定義如下:
~~~
on?::?(b?->?b?->?c)?->?(a?->?b)?->?a?->?a?->?c???
f?`on`?g?=?\x?y?->?f?(g?x)?(g?y)
~~~
執行`(==) `on` (> 0)`得到的函數就與`\x y -> (x > 0) == (y > 0)`基本等價。`on`與帶By的函數在一起會非常好用,你可以這樣寫:
~~~
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都取一個函數來比較兩個元素的大小。像sortBy的類型聲明為:`sortBy :: (a -> a -> Ordering) -> [a] -> [a]`。前面提過,Ordering類型可以有三個值,`LT`,`EQ`和`GT`。`compare`取兩個Ord類型類的元素作參數,所以`sort`與`sortBy compare`等價.
List是可以比較大小的,且比較的依據就是其中元素的大小。如果按照其子List的長度為標準當如何? 很好,你可能已經猜到了,sortBy函數.
~~~
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在這里的原理,就可以認為它與`\x y -> length x `compare` length y`等價。通常,與帶By的函數打交道時,若要判斷相等性,則`(==) `on` something`。若要判定大小,則`compare `on` something`.
## Data.Char
如其名,`Data.Char`模塊包含了一組用于處理字符的函數。由于字符串的本質就是一組字符的List,所以往往會在filter或是map字符串時用到它.
`Data.Char`模塊中含有一系列用于判定字符范圍的函數,如下:

isControl判斷一個字符是否是控制字符。
isSpace判斷一個字符是否是空格字符,包括空格,tab,換行符等.
isLower判斷一個字符是否為小寫.
isUper判斷一個字符是否為大寫。
isAlpha判斷一個字符是否為字母.
isAlphaNum判斷一個字符是否為字母或數字.
isPrint判斷一個字符是否是可打印的.
isDigit判斷一個字符是否為數字.
isOctDigit判斷一個字符是否為八進制數字.
isHexDigit判斷一個字符是否為十六進制數字.
isLetter判斷一個字符是否為字母.
isMark判斷是否為unicode注音字符,你如果是法國人就會經常用到的.
isNumber判斷一個字符是否為數字.
isPunctuation判斷一個字符是否為標點符號.
isSymbol判斷一個字符是否為貨幣符號.
isSeperater判斷一個字符是否為unicode空格或分隔符.
isAscii判斷一個字符是否在unicode字母表的前128位。
isLatin1判斷一個字符是否在unicode字母表的前256位.
isAsciiUpper判斷一個字符是否為大寫的ascii字符.
isAsciiLower判斷一個字符是否為小寫的ascii字符.
以上所有判斷函數的類型聲明皆為`Char -> Bool`,用到它們的絕大多數情況都無非就是過濾字符串或類似操作。假設我們在寫個程序,它需要一個由字符和數字組成的用戶名。要實現對用戶名的檢驗,我們可以結合使用`Data.List`模塊的all函數與`Data.Char`的判斷函數.
~~~
ghci>?all?isAlphaNum?"bobby283"???
True???
ghci>?all?isAlphaNum?"eddy?the?fish!"???
False
~~~
Kewl~ 免得你忘記,`all`函數取一個判斷函數和一個List做參數,若該List的所有元素都符合條件,就返回`True`.
也可以使用`isSpace`來實現`Data.List`的`words`函數.
~~~
ghci>?words?"hey?guys?its?me"???
["hey","guys","its","me"]???
ghci>?groupBy?((==)?`on`?isSpace)?"hey?guys?its?me"???
["hey","?","guys","?","its","?","me"]???
ghci>
~~~
Hmm,不錯,有點`words`的樣子了。只是還有空格在里面,恩,該怎么辦? 我知道,用`filter`濾掉它們!
啊哈.
`Data.Char`中也含有與`Ordering`相似的類型。`Ordering`可以有兩個值,`LT`,`GT`和`EQ`。這就是個枚舉,它表示了兩個元素作比較可能的結果.GeneralCategory類型也是個枚舉,它表示了一個字符可能所在的分類。而得到一個字符所在分類的主要方法就是使用`generalCategory`函數.它的類型為:`generalCategory :: Char -> GeneralCategory`。那31個分類就不在此一一列出了,試下這個函數先:
~~~
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將一個字符轉為大寫字母,若該字符不是小寫字母,就按原值返回.
toLower將一個字符轉為小寫字母,若該字符不是大寫字母,就按原值返回.
toTitle將一個字符轉為title-case,對大多數字符而言,title-case就是大寫.
digitToInt將一個字符轉為Int值,而這一字符必須得在`'1'..'9','a'..'f'`或`'A'..'F'`的范圍之內.
~~~
ghci>?map?digitToInt?"34538"???
[3,4,5,3,8]???
ghci>?map?digitToInt?"FF85AB"???
[15,15,8,5,10,11]
~~~
`intToDigit`是`digitToInt`的反函數。它取一個`0`到`15`的`Int`值作參數,并返回一個小寫的字符.
~~~
ghci>?intToDigit?15???
'f'???
ghci>?intToDigit?5???
'5'
~~~
ord與char函數可以將字符與其對應的數字相互轉換.
~~~
ghci>?ord?'a'???
97???
ghci>?chr?97???
'a'???
ghci>?map?ord?"abcdefgh"???
[97,98,99,100,101,102,103,104]
~~~
兩個字符的`ord`值之差就是它們在unicode字符表上的距離.
_Caesar ciphar_是加密的基礎算法,它將消息中的每個字符都按照特定的字母表進行替換。它的實現非常簡單,我們這里就先不管字母表了.
~~~
encode?::?Int?->?String?->?String???
encode?shift?msg?=??
??let?ords?=?map?ord?msg???
??shifted?=?map?(+?shift)?ords???
??in?map?chr?shifted
~~~
先將一個字符串轉為一組數字,然后給它加上某數,再轉回去。如果你是標準的組合牛仔,大可將函數寫為:`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&"
~~~
不錯。再簡單地將它轉成一組數字,減去某數后再轉回來就是解密了.
~~~
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
關聯列表(也叫做字典)是按照鍵值對排列而沒有特定順序的一種List。例如,我們用關聯列表儲存電話號碼,號碼就是值,人名就是鍵。我們并不關心它們的存儲順序,只要能按人名得到正確的號碼就好.在haskell中表示關聯列表的最簡單方法就是弄一個二元組的List,而這二元組就首項為鍵,后項為值。如下便是個表示電話號碼的關聯列表:
~~~
phoneBook?=?[("betty","555-2938")?,?
?????????????("bonnie","452-2928")?,?
?????????????("patsy","493-2928")?,?
?????????????("lucille","205-2928")?,?
?????????????("wendy","939-8282")?,?
?????????????("penny","853-2492")?]
~~~
不理這貌似古怪的縮進,它就是一組二元組的List而已。話說對關聯列表最常見的操作就是按鍵索值,我們就寫個函數來實現它。
~~~
findKey?::?(Eq?k)?=>?k?->?[(k,v)]?->?v??
findKey?key?xs?=?snd?.?head?.?filter?(\(k,v)?->?key?==?k)?$?xs
~~~

簡潔漂亮。這個函數取一個鍵和List做參數,過濾這一List僅保留鍵匹配的項,并返回首個鍵值對。但若該關聯列表中不存在這個鍵那會怎樣? 哼,那就會在試圖從空List中取head時引發一個運行時錯誤。無論如何也不能讓程序就這么輕易地崩潰吧,所以就應該用Maybe類型。如果沒找到相應的鍵,就返回Nothing。而找到了就返回`Just something`。而這something就是鍵對應的值。
~~~
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
~~~
看這類型聲明,它取一個可判斷相等性的鍵和一個關聯列表做參數,可能(Maybe)得到一個值。聽起來不錯.這便是個標準的處理List的遞歸函數,邊界條件,分割List,遞歸調用,都有了 -- 經典的fold模式。
看看用fold怎樣實現吧。
~~~
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來替代類似的遞歸函數會更好些。用fold的代碼讓人一目了然,而看明白遞歸則得多花點腦子。
~~~
ghci>?findKey?"penny"?phoneBook??
Just?"853-2492"??
ghci>?findKey?"betty"?phoneBook??
Just?"555-2938"??
ghci>?findKey?"wilma"?phoneBook??
Nothing
~~~
如魔咒般靈驗! 只要我們有這姑娘的號碼就Just可以得到,否則就是Nothing.方才我們實現的函數便是Data.List模塊的lookup,如果要按鍵去尋找相應的值,它就必須得遍歷整個List,直到找到為止。而`Data.Map`模塊提供了更高效的方式(通過樹實現),并提供了一組好用的函數。從現在開始,我們扔掉關聯列表,改用map.由于`Data.Map`中的一些函數與Prelude和`Data.List`模塊存在命名沖突,所以我們使用`qualified import`。`import qualified Data.Map as Map`在代碼中加上這句,并load到ghci中.繼續前進,看看`Data.Map`是如何的一座寶庫!
如下便是其中函數的一瞥:
fromList取一個關聯列表,返回一個與之等價的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)]
~~~
若其中存在重復的鍵,就將其忽略。如下即`fromList`的類型聲明。
~~~
Map.fromList?::?(Ord?k)?=>?[(k,v)]?->?Map.Map?k?v
~~~
這表示它取一組鍵值對的List,并返回一個將k映射為v的map。注意一下,當使用普通的關聯列表時,只需要鍵的可判斷相等性就行了。而在這里,它還必須得是可排序的。這在Data.Map模塊中是強制的。因為它會按照某順序將其組織在一棵樹中.在處理鍵值對時,只要鍵的類型屬于Ord類型類,就應該盡量使用Data.Map.empty返回一個空map.
~~~
ghci>?Map.empty??
fromList?[]
~~~
insert取一個鍵,一個值和一個map做參數,給這個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取一個鍵值對做參數,并返回一個只含有一個映射的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,如果它找到鍵對應的值。就返回`Just something`,否則返回`Nothing`。
member是個判斷函數,它取一個鍵與map做參數,并返回該鍵是否存在于該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與其對應的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的反函數。
~~~
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`是個很酷的小函數,它與fromList很像,只是它不會直接忽略掉重復鍵,而是交給一個函數來處理它們。假設一個姑娘可以有多個號碼,而我們有個像這樣的關聯列表:
~~~
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"
~~~
一旦出現重復鍵,這個函數會將不同的值組在一起,同樣,也可以默認地將每個值放到一個單元素的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"]
~~~
很簡潔! 它還有別的玩法,例如在遇到重復元素時,單選最大的那個值.
~~~
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若已經包含這個鍵,就問問這個函數該怎么辦。
~~~
ghci>?Map.insertWith?(+)?3?100?$?Map.fromList?[(3,4),(5,103),(6,339)]??
fromList?[(3,104),(5,103),(6,339)]
~~~
`Data.Map`里面還有不少函數,這個文檔中的列表就很全了.
## Data.Set

`Data.Set`模塊提供了對數學中集合的處理。集合既像List也像Map: 它里面的每個元素都是唯一的,且內部的數據由一棵樹來組織(這和Data.Map模塊的map很像), 必須得是可排序的。同樣是插入,刪除,判斷從屬關系之類的操作,使用集合要比List快得多。對一個集合而言,最常見的操作莫過于并集,判斷從屬或是將集合轉為List
由于Data.Set模塊與Prelude模塊和Data.List模塊中存在大量的命名沖突,所以我們使用_qualified import_
將import語句至于代碼之中:
~~~
import?qualified?Data.Set?as?Set
~~~
然后在GHci中裝載
假定我們有兩個字符串,要找出同時存在于兩個字符串的字符
~~~
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函數同你想的一樣,它取一個List作參數并將其轉為一個集合
~~~
ghci>?let?set1?=?Set.fromList?text1???
ghci>?let?set2?=?Set.fromList?text2???
ghci>?set1???
fromList?"?.?AIRadefhijlmnorstuy"???
ghci>?set2???
fromList?"?!Tabcdefghilmnorstuvwy"
~~~
如你所見,所有的元素都被排了序。而且每個元素都是唯一的。現在我們取它的交集看看它們共同包含的元素:
~~~
ghci>?Set.intersection?set1?set2???
fromList?"?adefhilmnorstuy"
~~~
使用`difference`函數可以得到存在于第一個集合但不在第二個集合的元素
~~~
ghci>?Set.difference?set1?set2???
fromList?".?AIRj"???
ghci>?Set.difference?set2?set1???
fromList?"!Tbcgvw"
~~~
也可以使用`union`得到兩個集合的并集
~~~
ghci>?Set.union?set1?set2???
fromList?"?!.?AIRTabcdefghijlmnorstuvwy"
~~~
`null`,`size`,`member`,`empty`,`singleton`,`insert`,`delete`這幾個函數就跟你想的差不多啦
~~~
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
~~~
對集合也可以執行`map`和`filter`
~~~
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
~~~
集合有一常見用途,那就是先`fromList`刪掉重復元素后再`toList`轉回去。盡管Data.List模塊的`nub`函數完全可以完成這一工作,但在對付大List時則會明顯的力不從心。使用集合則會快很多,`nub`函數只需List中的元素屬于Eq類型類就行了,而若要使用集合,它必須得屬于Ord類型類
~~~
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]
~~~
在處理較大的List時,`setNub`要比`nub`快,但也可以從中看出,`nub`保留了List中元素的原有順序,而`setNub`不。
## 構造自己的模塊
我們已經見識過了幾個很酷的模塊,但怎樣才能構造自己的模塊呢? 幾乎所有的編程語言都允許你將代碼分成多個文件,haskell也不例外。在編程時,將功能相近的函數和類型至于同一模塊中會是個很好的習慣。這樣一來,你就可以輕松地一個import來重用其中的函數.
接下來我們將構造一個由計算機幾何圖形體積和面積組成的模塊,先從新建一個`Geometry.hs`的文件開始.
在模塊的開頭定義模塊的名稱,如果文件名叫做`Geometry.hs`那它的名字就得是_Geometry_。在聲明出它含有的函數名之后就可以編寫函數的實現啦,就這樣寫:
~~~
module?Geometry???
(?sphereVolume???
,sphereArea???
,cubeVolume???
,cubeArea???
,cuboidArea???
,cuboidVolume???
)?where
~~~
如你所見,我們提供了對球體,立方體和立方體的面積和體積的解法。繼續進發,定義函數體:
~~~
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
~~~

標準的幾何公式。有幾個地方需要注意一下,由于立方體只是長方體的特殊形式,所以在求它面積和體積的時候我們就將它當作是邊長相等的長方體。在這里還定義了一個helper函數,`rectangleArea`它可以通過長方體的兩條邊計算出長方體的面積。它僅僅是簡單的相乘而已,分量不大。但請注意我們可以在這一模塊中調用這個函數,而它不會被導出! 因為我們這個模塊只與三維圖形打交道.
當構造一個模塊的時 候,我們通常只會導出那些行為相近的函數,而其內部的實現則是隱蔽的。如果有人用到了Geometry模塊,就不需要關心它的內部實現是如何。我們作為編寫者,完全可以隨意修改這些函數甚至將其刪掉,沒有人會注意到里面的變動,因為我們并不把它們導出.
要使用我們的模塊,只需:
~~~
import?Geometry
~~~
將Geometry.hs文件至于用到它的程序文件的同一目錄之下.
模塊也可以按照分層的結構來組織,每個模塊都可以含有多個子模塊。而子模塊還可以有自己的子模塊。我們可以把Geometry分成三個子模塊,而一個模塊對應各自的圖形對象.
首先,建立一個Geometry文件夾,注意首字母要大寫,在里面新建三個文件
如下就是各個文件的內容:
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。對Cuboid也是同樣,也注意下,在三個模塊中我們定義了許多名稱相同的函數,因為所在模塊不同,所以不會產生命名沖突。若要在Geometry.Cube使用Geometry.Cuboid中的函數,就不能直接import Geometry.Cuboid,而必須得qualified import。因為它們中間的函數名完全相同.
~~~
import?Geometry.Sphere
~~~
然后,調用`area`和`volume`,就可以得到球體的面積和體積,而若要用到兩個或更多此類模塊,就必須得`qualified import`來避免重名。所以就得這樣寫:
~~~
import?qualified?Geometry.Sphere?as?Sphere???
import?qualified?Geometry.Cuboid?as?Cuboid???
import?qualified?Geometry.Cube?as?Cube
~~~
然后就可以調用`Sphere.area`,`Sphere.volume`,`Cuboid.area`了,而每個函數都只計算其對應物體的面積和體積.
以后你若發現自己的代碼體積龐大且函數眾多,就應該試著找找目的相近的函數能否裝入各自的模塊,也方便日后的重用.