模組 (Modules)

裝載模組

Haskell 中的模組是含有一組相關的函數,型別和型別類的組合。而 Haskell 程序的本質便是從主模組中引用其它模組並呼叫其中的函數來執行操作。這樣可以把程式碼分成多塊,只要一個模組足夠的獨立,它裡面的函數便可以被不同的程序反覆重用。這就讓不同的程式碼各司其職,提高了程式碼的健壯性。
Haskell 的標準庫就是一組模組,每個模組都含有一組功能相近或相關的函數和型別。有處理 List 的模組,有處理並發的模組,也有處理複數的模組,等等。目前為止我們談及的所有函數,型別以及型別類都是 Prelude 模組的一部分,它預設自動裝載。在本章,我們看一下幾個常用的模組,在開始瀏覽其中的函數之前,我們先得知道如何裝載模組.
在 Haskell中,裝載模組的語法為 import,這必須得在函數的定義之前,所以一般都是將它置於程式碼的頂部。無疑,一段程式碼中可以裝載很多模組,只要將 import 語句分行寫開即可。裝載 Data.List 試下,它裡面有很多實用的 List 處理函數.
執行 import Data.List,這樣一來 Data.List 中包含的所有函數就都進入了全局命名空間。也就是說,你可以在程式碼的任意位置呼叫這些函數.Data.List 模組中有個 nub 函數,它可以篩掉一個 List 中的所有重複元素。用點號將 lengthnub 組合: length . nub,即可得到一個與 (\xs -> length (nub xs)) 等價的函數。
1
import Data.List
2
3
numUniques :: (Eq a) => [a] -> Int
4
numUniques = length . nub
Copied!
你也可以在 ghci 中裝載模組,若要呼叫 Data.List 中的函數,就這樣:
1
ghci> :m Data.List
Copied!
若要在 ghci 中裝載多個模組,不必多次 :m 命令,一下就可以全部搞定:
1
ghci> :m Data.List Data.Map Data.Set
Copied!
而你的程序中若已經有包含的程式碼,就不必再用 :m 了.
如果你只用得到某模組的兩個函數,大可僅包含它倆。若僅裝載 Data.List 模組 nubsort,就這樣:
1
import Data.List (nubsort)
Copied!
也可以只包含除去某函數之外的其它函數,這在避免多個模組中函數的命名衝突很有用。假設我們的程式碼中已經有了一個叫做 nub 的函數,而裝入 Data.List 模組時就要把它裡面的 nub 除掉.
1
import Data.List hiding (nub)
Copied!
避免命名衝突還有個方法,便是 qualified importData.Map 模組提供一了一個按鍵索值的資料結構,它裡面有幾個和 Prelude 模組重名的函數。如 filternull,裝入 Data.Map 模組之後再呼叫 filter,Haskell 就不知道它究竟是哪個函數。如下便是解決的方法:
1
import qualified Data.Map
Copied!
這樣一來,再呼叫 Data.Map 中的 filter 函數,就必須得 Data.Map.filter,而 filter 依然是為我們熟悉喜愛的樣子。但是要在每個函數前面都加 個Data.Map 實在是太煩人了! 那就給它起個別名,讓它短些:
1
import qualified Data.Map as M
Copied!
好,再呼叫 Data.Map 模組的 filter 函數的話僅需 M.filter 就行了
要瀏覽所有的標準庫模組,參考這個手冊。翻閲標準庫中的模組和函數是提升個人 Haskell 水平的重要途徑。你也可以各個模組的原始碼,這對 Haskell 的深入學習及掌握都是大有好處的.
檢索函數或搜尋函數位置就用 [http://www.Haskell.org/hoogle/ Hoogle],相當了不起的 Haskell 搜索引擎! 你可以用函數名,模組名甚至型別聲明來作為檢索的條件.

Data.List

顯而易見,Data.List 是關於 List 操作的模組,它提供了一組非常有用的 List 處理函數。在前面我們已經見過了其中的幾個函數(如 mapfilter),這是 Prelude 模組出於方便起見,導出了幾個 Data.List 裡的函數。因為這幾個函數是直接引用自 Data.List,所以就無需使用 qualified import。在下面,我們來看看幾個以前沒見過的函數:
intersperse 取一個元素與 List 作參數,並將該元素置於 List 中每對元素的中間。如下是個例子:
1
ghci> intersperse '.' "MONKEY"
2
"M.O.N.K.E.Y"
3
ghci> intersperse 0 [1,2,3,4,5,6]
4
[1,0,2,0,3,0,4,0,5,0,6]
Copied!
intercalate 取兩個 List 作參數。它會將第一個 List 交叉插入第二個 List 中間,並返回一個 List.
1
ghci> intercalate " " ["hey","there","guys"]
2
"hey there guys"
3
ghci> intercalate [0,0,0] [[1,2,3],[4,5,6],[7,8,9]]
4
[1,2,3,0,0,0,4,5,6,0,0,0,7,8,9]
Copied!
transpose 函數可以反轉一組 List 的 List。你若把一組 List 的 List 看作是個 2D 的矩陣,那 transpose 的操作就是將其列為行。
1
ghci> transpose [[1,2,3],[4,5,6],[7,8,9]]
2
[[1,4,7],[2,5,8],[3,6,9]]
3
ghci> transpose ["hey","there","guys"]
4
["htg","ehu","yey","rs","e"]
Copied!
假如有兩個多項式 3x<sup>2</sup> + 5x + 910x<sup>3</sup> + 98x<sup>3</sup> + 5x<sup>2</sup> + x - 1,將其相加,我們可以列三個 List: [0,3,5,9][10,0,0,9][8,5,1,-1] 來表示。再用如下的方法取得結果.
1
ghci> map sum $ transpose [[0,3,5,9],[10,0,0,9],[8,5,1,-1]]
2
[18,8,6,17]
Copied!
使用 transpose 處理這三個 List 之後,三次冪就到了第一行,二次冪到了第二行,以此類推。在用 sum 函數將其映射,即可得到正確的結果。
foldl'foldl1' 是它們各自惰性實現的嚴格版本。在用 fold 處理較大的 List 時,經常會遇到堆棧溢出的問題。而這罪魁禍首就是 fold 的惰性: 在執行 fold 時,累加器的值並不會被立即更新,而是做一個"在必要時會取得所需的結果"的承諾。每過一遍累加器,這一行為就重複一次。而所有的這堆"承諾"最終就會塞滿你的堆棧。嚴格的 fold 就不會有這一問題,它們不會作"承諾",而是直接計算中間值的結果並繼續執行下去。如果用惰性 fold 時經常遇到溢出錯誤,就應換用它們的嚴格版。
concat 把一組 List 連接為一個 List。
1
ghci> concat ["foo","bar","car"]
2
"foobarcar"
3
ghci> concat [[3,4,5],[2,3,4],[2,1,1]]
4
[3,4,5,2,3,4,2,1,1]
Copied!
它相當於移除一級嵌套。若要徹底地連接其中的元素,你得 concat 它兩次才行.
concatMap 函數與 map 一個 List 之後再 concat 它等價.
1
ghci> concatMap (replicate 4) [1..3]
2
[1,1,1,1,2,2,2,2,3,3,3,3]
Copied!
and 取一組布林值 List 作參數。只有其中的值全為 True 的情況下才會返回 True
1
ghci> and $ map (>4) [5,6,7,8]
2
True
3
ghci> and $ map (==4) [4,4,4,3,4]
4
False
Copied!
orand 相似,一組布林值 List 中若存在一個 True 它就返回 True.
1
ghci> or $ map (==4) [2,3,4,5,6,1]
2
True
3
ghci> or $ map (>4) [1,2,3]
4
False
Copied!
anyall 取一個限制條件和一組布林值 List 作參數,檢查是否該 List 的某個元素或每個元素都符合該條件。通常較 map 一個 List 到 andor 而言,使用 anyall 會更多些。
1
ghci> any (==4) [2,3,5,6,1,4]
2
True
3
ghci> all (>4) [6,9,10]
4
True
5
ghci> all (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
6
False
7
ghci> any (`elem` ['A'..'Z']) "HEYGUYSwhatsup"
8
True
Copied!
iterate 取一個函數和一個值作參數。它會用該值去呼叫該函數並用所得的結果再次呼叫該函數,產生一個無限的 List.
1
ghci> take 10 $ iterate (*2) 1
2
[1,2,4,8,16,32,64,128,256,512]
3
ghci> take 3 $ iterate (++ "haha") "haha"
4
["haha","hahahaha","hahahahahaha"]
Copied!
splitAt 取一個 List 和數值作參數,將該 List 在特定的位置斷開。返回一個包含兩個 List 的二元組.
1
ghci> splitAt 3 "heyman"
2
("hey","man")
3
ghci> splitAt 100 "heyman"
4
("heyman","")
5
ghci> splitAt (-3) "heyman"
6
("","heyman")
7
ghci> let (a,b) = splitAt 3 "foobar" in b ++ a
8
"barfoo"
Copied!
takeWhile 這一函數十分的實用。它從一個 List 中取元素,一旦遇到不符合條件的某元素就停止.
1
ghci> takeWhile (>3) [6,5,4,3,2,1,2,3,4,5,4,3,2,1]
2
[6,5,4]
3
ghci> takeWhile (/=' ') "This is a sentence"
4
"This"
Copied!
如果要求所有三次方小於 10000 的數的和,用 filter 來過濾 map (^3) [1..] 所得結果中所有小於 10000 的數是不行的。因為對無限 List 執行的 filter 永遠都不會停止。你已經知道了這個 List 是單增的,但 Haskell 不知道。所以應該這樣:
1
ghci> sum $ takeWhile (<10000) $ map (^3) [1..]
2
53361
Copied!
(^3) 處理一個無限 List,而一旦出現了大於等於 10000 的元素這個 List 就被切斷了,sum 到一起也就輕而易舉.
dropWhile 與此相似,不過它是扔掉符合條件的元素。一旦限制條件返回 False,它就返回 List 的餘下部分。方便實用!
1
ghci> dropWhile (/=' ') "This is a sentence"
2
" is a sentence"
3
ghci> dropWhile (<3) [1,2,2,2,3,4,5,4,3,2,1]
4
[3,4,5,4,3,2,1]
Copied!
給一 Tuple 組成的 List,這 Tuple 的首项表示股票價格,第二三四項分別表示年,月,日。我們想知道它是在哪天首次突破 $1000 的!
1
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)]
2
ghci> head (dropWhile (\(val,y,m,d) -> val < 1000) stock)
3
(1001.4,2008,9,4)
Copied!
spantakeWhile 有點像,只是它返回兩個 List。第一個 List 與同參數呼叫 takeWhile 所得的結果相同,第二個 List 就是原 List 中餘下的部分。
1
ghci> let (fwrest) = span (/=' ') "This is a sentence" in "First word:" ++ fw ++ ",the rest:" ++ rest
2
"First word: This,the rest: is a sentence"
Copied!
span 是在條件首次為 False 時斷開 List,而 break 則是在條件首次為 True 時斷開 Listbreak pspan (not . p) 是等價的.
1
ghci> break (==4) [1,2,3,4,5,6,7]
2
([1,2,3],[4,5,6,7])
3
ghci> span (/=4) [1,2,3,4,5,6,7]
4
([1,2,3],[4,5,6,7])
Copied!
break 返回的第二個 List 就會以第一個符合條件的元素開頭。
sort 可以排序一個 List,因為只有能夠作比較的元素才可以被排序,所以這一 List 的元素必須是 Ord 型別類的實例型別。
1
ghci> sort [8,5,3,2,1,6,4,2]
2
[1,2,2,3,4,5,6,8]
3
ghci> sort "This will be sorted soon"
4
" Tbdeehiillnooorssstw"
Copied!
group 取一個 List 作參數,並將其中相鄰並相等的元素各自歸類,組成一個個子 List.
1
ghci> group [1,1,1,1,2,2,2,2,3,3,2,2,2,5,6,7]
2
[[1,1,1,1],[2,2,2,2],[3,3],[2,2,2],[5],[6],[7]]
Copied!
若在 group 一個 List 之前給它排序就可以得到每個元素在該 List 中的出現次數。
1
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]
2
[(1,4),(2,7),(3,2),(5,1),(6,1),(7,1)]
Copied!
initstailsinittail 相似,只是它們會遞歸地呼叫自身直到什麼都不剩,看:
1
ghci> inits "w00t"
2
["","w","w0","w00","w00t"]
3
ghci> tails "w00t"
4
["w00t","00t","0t","t",""]
5
ghci> let w = "w00t" in zip (inits w) (tails w)
6
[("","w00t"),("w","00t"),("w0","0t"),("w00","t"),("w00t","")]
Copied!
我們用 fold 實現一個搜索子 List 的函數:
1
search :: (Eq a) => [a] -> [a] -> Bool
2
search needle haystack =
3
let nlen = length needle
4
in foldl (\acc x -> if take nlen x == needle then True else acc) False (tails haystack)
Copied!
首先,對搜索的 List 呼叫 tails,然後遍歷每個 List 來檢查它是不是我們想要的.
由此我們便實現了一個類似 isInfixOf 的函數,isInfixOf 從一個 List 中搜索一個子 List,若該 List 包含子 List,則返回 True.
1
ghci> "cat" `isInfixOf` "im a cat burglar"
2
True
3
ghci> "Cat" `isInfixOf` "im a cat burglar"
4
False
5
ghci> "cats" `isInfixOf` "im a cat burglar"
6
False
Copied!
isPrefixOfisSuffixOf 分別檢查一個 List 是否以某子 List 開頭或者結尾.
1
ghci> "hey" `isPrefixOf` "hey there!"
2
True
3
ghci> "hey" `isPrefixOf` "oh hey there!"
4
False
5
ghci> "there!" `isSuffixOf` "oh hey there!"
6
True
7
ghci> "there!" `isSuffixOf` "oh hey there"
8
False
Copied!
elemnotElem 檢查一個 List 是否包含某元素.
partition 取一個限制條件和 List 作參數,返回兩個 List,第一個 List 中包含所有符合條件的元素,而第二個 List 中包含餘下的.
1
ghci> partition (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
2
("BOBMORGAN","sidneyeddy")
3
ghci> partition (>3) [1,3,5,6,3,2,1,0,3,7]
4
([5,6,7],[1,3,3,2,1,0,3])
Copied!
瞭解這個與 spanbreak 的差異是很重要的.
1
ghci> span (`elem` ['A'..'Z']) "BOBsidneyMORGANeddy"
2
("BOB","sidneyMORGANeddy")
Copied!
spanbreak 會在遇到第一個符合或不符合條件的元素處斷開,而 partition 則會遍歷整個 List。
find 取一個 List 和限制條件作參數,並返回首個符合該條件的元素,而這個元素是個 Maybe 值。在下章,我們將深入地探討相關的算法和資料結構,但在這裡你只需瞭解 Maybe 值是 Just somethingNothing 就夠了。與一個 List 可以為空也可以包含多個元素相似,一個 Maybe 可以為空,也可以是單一元素。同樣與 List 類似,一個 Int 型的 List 可以寫作 [Int]Maybe有個 Int 型可以寫作 Maybe Int。先試一下 find 函數再說.
1
ghci> find (>4) [1,2,3,4,5,6]
2
Just 5
3
ghci> find (>9) [1,2,3,4,5,6]
4
Nothing
5
ghci> :t find
6
find :: (a -> Bool) -> [a] -> Maybe a
Copied!
注意一下 find 的型別,它的返回結果為 Maybe a,這與 [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.
elemIndexelem 相似,只是它返回的不是布林值,它只是'可能' (Maybe)返回我們找的元素的索引,若這一元素不存在,就返回 Nothing
1
ghci> :t elemIndex
2
elemIndex :: (Eq a) => a -> [a] -> Maybe Int
3
ghci> 4 `elemIndex` [1,2,3,4,5,6]
4
Just 3
5
ghci> 10 `elemIndex` [1,2,3,4,5,6]
6
Nothing
Copied!
elemIndiceselemIndex 相似,只不過它返回的是 List,就不需要 Maybe 了。因為不存在用空 List 就可以表示,這就與 Nothing 相似了.
1
ghci> ' ' `elemIndices` "Where are the spaces?"
2
[5,9,13]
Copied!
findIndexfind 相似,但它返回的是可能存在的首個符合該條件元素的索引。findIndices 會返回所有符合條件的索引.
1
ghci> findIndex (==4) [5,3,2,1,6,4]
2
Just 5
3
ghci> findIndex (==7) [5,3,2,1,6,4]
4
Nothing
5
ghci> findIndices (`elem` ['A'..'Z']) "Where Are The Caps?"
6
[0,6,10,14]
Copied!
在前面,我們講過了 zipzipWith,它們只能將兩個 List 組到一個二元組數或二參函數中,但若要組三個 List 該怎麼辦? 好說~ 有 zip3,zip4...,和 zipWith3, zipWith4...直到 7。這看起來像是個 hack,但工作良好。連着組 8 個 List 的情況很少遇到。還有個聰明辦法可以組起無限多個 List,但限於我們目前的水平,就先不談了.
1
ghci> zipWith3 (\x y z -> x + y + z) [1,2,3] [4,5,2,2] [2,2,3]
2
[7,9,8]
3
ghci> zip4 [2,3,3] [2,2,2] [5,5,3] [2,2,2]
4
[(2,2,5,2),(3,2,5,2),(3,2,3,2)]
Copied!
與普通的 zip 操作相似,以返回的 List 中長度最短的那個為準.
在處理來自檔案或其它地方的輸入時,lines 會非常有用。它取一個字串作參數。並返回由其中的每一行組成的 List.
1
ghci> lines "first line\nsecond line\nthird line"
2
["first line","second line","third line"]
Copied!
'\n' 表示unix下的換行符,在 Haskell 的字元中,反斜杠表示特殊字元.
unlineslines 的反函數,它取一組字串的 List,並將其通過 '\n'合併到一塊.
1
ghci> unlines ["first line""second line""third line"]
2
"first line\nsecond line\nthird line\n"
Copied!
wordsunwords 可以把一個字串分為一組單詞或執行相反的操作,很有用.
1
ghci> words "hey these are the words in this sentence"
2
["hey","these","are","the","words","in","this","sentence"]
3
ghci> words "hey these are the words in this\nsentence"
4
["hey","these","are","the","words","in","this","sentence"]
5
ghci> unwords ["hey","there","mate"]
6
"hey there mate"
Copied!
我們前面講到了 nub,它可以將一個 List 中的重複元素全部篩掉,使該 List 的每個元素都如雪花般獨一無二,'nub' 的含義就是'一小塊'或'一部分',用在這裡覺得很古怪。我覺得,在函數的命名上應該用更確切的詞語,而避免使用老掉牙的過時詞彙.
1
ghci> nub [1,2,3,4,3,2,1,2,3,4,3,2,1]
2
[1,2,3,4]
3
ghci> nub "Lots of words and stuff"
4
"Lots fwrdanu"
Copied!
delete 取一個元素和 List 作參數,會刪掉該 List 中首次出現的這一元素.
1
ghci> delete 'h' "hey there ghang!"
2
"ey there ghang!"
3
ghci> delete 'h' . delete 'h' $ "hey there ghang!"
4
"ey tere ghang!"
5
ghci> delete 'h' . delete 'h' . delete 'h' $ "hey there ghang!"
6
"ey tere gang!"
Copied!
\ 表示 List 的差集操作,這與集合的差集很相似,它會從左邊 List 中的元素扣除存在於右邊 List 中的元素一次.
1
ghci> [1..10] \\ [2,5,9]
2
[1,3,4,6,7,8,10]
3
ghci> "Im a big baby" \\ "big"
4
"Im a baby"
Copied!
union 與集合的並集也是很相似,它返回兩個 List 的並集,即遍歷第二個 List 若存在某元素不屬於第一個 List,則追加到第一個 List。看,第二個 List 中的重複元素就都沒了!
1
ghci> "hey man" `union` "man what's up"
2
"hey manwt'sup"
3
ghci> [1..7] `union` [5..10]
4
[1,2,3,4,5,6,7,8,9,10]
Copied!
intersect 相當於集合的交集。它返回兩個 List 的相同部分.
1
ghci> [1..7] `intersect` [5..10]
2
[5,6,7]
Copied!
insert 可以將一個元素插入一個可排序的 List,並將其置於首個大於等於它的元素之前,如果使用 insert 來給一個排過序的 List 插入元素,返回的結果依然是排序的.
1
ghci> insert 4 [1,2,3,5,6,7]
2
[1,2,3,4,5,6,7]
3
ghci> insert 'g' $ ['a'..'f'] ++ ['h'..'z']
4
"abcdefghijklmnopqrstuvwxyz"
5
ghci> insert 3 [1,2,4,3,2,1]
6
[1,2,3,4,3,2,1]
Copied!
lengthtakedropsplitAt!!replicate 之類的函數有個共同點。那就是它們的參數中都有個 Int 值(或者返回Int值),我覺得使用 Intergal 或 Num 型別類會更好,但出於歷史原因,修改這些會破壞掉許多既有的程式碼。在 Data.List 中包含了更通用的替代版,如: genericLength,genericTake,genericDrop,genericSplitAt,genericIndexgenericReplicatelength 的型別聲明為 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, intsectgroup 函數也有各自的通用替代版 nubBydeleteByunionByintersectBygroupBy,它們的區別就是前一組函數使用 (==) 來測試是否相等,而帶 By 的那組則取一個函數作參數來判定相等性,group 就與 groupBy (==) 等價.
假如有個記錄某函數在每秒的值的 List,而我們要按照它小於零或者大於零的交界處將其分為一組子 List。如果用 group,它只能將相鄰並相等的元素組到一起,而在這裡我們的標準是它們是否互爲相反數。groupBy 登場! 它取一個含兩個參數的函數作為參數來判定相等性.
1
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]
2
ghci> groupBy (\x y -> (x > 0) == (y > 0)) values
3
[[-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]]
Copied!
這樣一來我們就可以很清楚地看出哪部分是正數,哪部分是負數,這個判斷相等性的函數會在兩個元素同時大於零或同時小於零時返回 True。也可以寫作 \x y -> (x > 0) && (y > 0) || (x <= 0) && (y <= 0)。但我覺得第一個寫法的可讀性更高。Data.Function 中還有個 on 函數可以讓它的表達更清晰,其定義如下:
1
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
2
f `on` g = \x y -> f (g x) (g y)
Copied!
執行 (==) `on` (> 0) 得到的函數就與 \x y -> (x > 0) == (y > 0) 基本等價。on 與帶 By 的函數在一起會非常好用,你可以這樣寫:
1
ghci> groupBy ((==) `on` (> 0)) values
2
[[-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]]
Copied!
可讀性很高! 你可以大聲念出來: 按照元素是否大於零,給它分類!
同樣,sortinsertmaximummin 都有各自的通用版本。如 groupBy 類似,sortByinsertBymaximumByminimumBy 都取一個函數來比較兩個元素的大小。像 sortBy 的型別聲明為: sortBy :: (a -> a -> Ordering) -> [a] -> [a]。前面提過,Ordering 型別可以有三個值,LTEQGTcompare 取兩個 Ord 型別類的元素作參數,所以 sortsortBy compare 等價.
List 是可以比較大小的,且比較的依據就是其中元素的大小。如果按照其子 List 的長度為標準當如何? 很好,你可能已經猜到了,sortBy 函數.
1
ghci> let xs = [[5,4,5,4,4],[1,2,3],[3,5,4,3],[],[2],[2,2]]
2
ghci> sortBy (compare `on` length) xs
3
[[],[2],[2,2],[1,2,3],[3,5,4,3],[5,4,5,4,4]]
Copied!
太絶了! 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 的判斷函數.
1
ghci> all isAlphaNum "bobby283"
2
True
3
ghci> all isAlphaNum "eddy the fish!"
4
False
Copied!
Kewl~ 免得你忘記,all 函數取一個判斷函數和一個 List 做參數,若該 List 的所有元素都符合條件,就返回 True.
也可以使用 isSpace 來實現 Data.Listwords 函數.
1
ghci> words "hey guys its me"
2
["hey","guys","its","me"]
3
ghci> groupBy ((==) `on` isSpace) "hey guys its me"
4
["hey"," ","guys"," ","its"," ","me"]
5
ghci>
Copied!
Hmm,不錯,有點 words 的樣子了。只是還有空格在裡面,恩,該怎麼辦? 我知道,用 filter 濾掉它們!
1
ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hey guys its me"
2
["hey","guys","its","me"]
Copied!
啊哈.
Data.Char 中也含有與 Ordering