07 Modules 模块

缺省模块

Prelude 模块是缺省模块,会被自动载入

载入模块

import 可以载入模块,模块载入后,其中的名字就都在命名空间中了。

比如 Data.List 模块中有一个 nub 函数,可以筛选掉列表中的重复元素,下面我们用 nublength 组合起来,写一个计算列表中非重复元素个数的函数

import Data.List  

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

在 ghci 中载入模块

:m 命令可以在 ghci 中载入模块

ghci> :m Data.List

可以一次载入多个模块

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

部分载入

如果我们想只载入模块中的部分名字,可以用括号将其列出来,比如只载入 Data.List 模块中的 nubsort 函数,可以向下面这样

import Data.List (nub,sort)

也可以用 hiding 指令排除掉模块中的某些名字,比如排除掉 nub 函数

import Data.List hiding (nub)

限定载入模块

如果要载入的模块中有和当前命名空间中重复的名字,则需要用 qualified 指令限定载入的模块。这时候再使用限定载入模块中的名字,就必须使用全限定名

比如 Data.Map 中就有和缺省模块中同样名字的 filter 函数,要使用它就必须进行限定

import qualified Data.Map

Data.Map.filter

使用全限定名字实在是太啰嗦了,所以我们可以用 as 指令给被限定的模块起个简短的别名

import qualified Data.Map as M

M.filter

Data.List

该模块提供了一组非常有用的 List 处理函数。出于方便起见,几个常用的函数(如 mapfilter)被引入到了 Prelude 模块中,可以直接使用。下面我们来看看几个以前没见过的函数

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

转置 2D 的矩阵

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

foldl' 和 foldl1'

它们是各自惰性实现的严格版本,如果用惰性 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]

concatMap

相当于先 map,然后对结果在 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

比 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

将一个列表在指定的的位置断开

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 无法处理无限 List ,所以应该这样:

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

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 是符合条件的列表,第二个 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"

break

span 是在条件首次为 False 时断开列表,而 break 则是在条件首次为 True 时断开 列表。

break pspan (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])

sort

排序一个 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

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 中的出现次数

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 配合 tails 实现一个在列表中搜索子列的函数

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)

运行效果

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 开头或者结尾

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

将一个列表分为两部分,一部分中包含所有符合条件的元素,另一部分包含余下的元素

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 不同,spanbreak 会在遇到第一个符合或不符合条件的元素处断开,而 partition 则会遍历整个 List。

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

find

在列表中查找并返回首个符合该条件的元素,返回的时候会将其转换为 Maybe 类型类

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

前面那段找股票的代码,head (dropWhile (\(val,y,m,d) -> val < 1000) stock) ,因为 dropWhile 可能会返回一个空 List,而对空 List 取 head 就会引发一个错误。

把它改成 find (\(val,y,m,d) -> val > 1000) stock 就安全多啦

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 相似,他会返回所有匹配位置组成的列表,而不仅仅是第一个匹配的位置

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]

zip3 和 zipWith3

前面讲过 zipzipWith,它们只能将两个列表,要处理三个以上的列表可以使用 zip3 , zip4 ... zipWith3 , zipWith4 ... 直到 7

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

lines

lines 在处理来自文件或其它地方的输入时 会非常有用

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

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 中的重复元素全部筛掉

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 中的元素一次

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中

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,并将其置于首个大于等于它的元素之前

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]

整数通用版函数

lengthtakedropsplitAt!!replicate 这些函数涉及到整数的时候使用的是 Int 类型,通用性很差,但出于历史原因,无法修改以前的代码。

在 Data.List 中包含了更通用的替代版,如: genericLengthgenericTakegenericDropgenericSplitAtgenericIndexgenericReplicate

比如 genericLength 的型别声明则为 genericLength :: (Num a) => [b] -> a,Num 既可以是整数又可以是浮点数,let xs = [1..6] in sum xs / genericLength xs 就不会有问题了

条件通用版函数

nub, delete, union, intsectgroup 等函数都是用相等性作为测试条件,通用替代版 nubBydeleteByunionByintersectBygroupBy 可以自己定义一个函数作为测试条件

比如,用 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]]

on 函数

使用 on 函数可以进一步精简代码, 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) 等价,上面的代码可以改成这样

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

比较通用版函数

sortinsertmaximummin 都有各自的通用版本。sortByinsertBymaximumByminimumBy,可以自定义比较函数

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

Data.Char

Data.Char 模块包含了一组用于处理字符的函数。

过滤函数

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 字符

以上函数配合 Data.List 模块的 all 过滤字符串非常好用

ghci> all isAlphaNum "bobby283"  
True  

ghci> all isAlphaNum "eddy the fish!"  
False

下面自定义一个 words 函数

ghci> words "hey guys its me"  
["hey","guys","its","me"]  

ghci> groupBy ((==) `on` isSpace) "hey guys its me"  
["hey"," ","guys"," ","its"," ","me"]  

ghci>

再加入过滤空格的功能

ghci> filter (not . any isSpace) . groupBy ((==) `on` isSpace) $ "hey guys its me"  
["hey","guys","its","me"]

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]

转换函数

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]

intToDigitdigitToInt 的反函数。它取一个 0 到 15 的 Int 值作参数,并返回一个小写的字符

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

ordchar 函数可以将字符与其对应的数字相互转换

ghci> ord 'a'  
97  

ghci> chr 97  
'a'  

ghci> map ord "abcdefgh"  
[97,98,99,100,101,102,103,104]

Data.Map

键值对对象

在 Haskell 中,键值对就是序对的列表,如下就是一个键值对对象

phoneBook = [("betty","555-2938") ,
             ("bonnie","452-2928") ,
             ("patsy","493-2928") ,
             ("lucille","205-2928") ,
             ("wendy","939-8282") ,
             ("penny","853-2492") ]

可以自己实现一个键值对查找函数

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

如果要查找的键不再关联列表中,filter 函数就会返回一个空列表, head 函数就会出现错误,因此应该用 Maybe 型别修改代码如下

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

这其实是一个经典的 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

试用一下,看看效果

ghci> findKey "penny" phoneBook 
Just "853-2492" 

ghci> findKey "betty" phoneBook 
Just "555-2938" 

ghci> findKey "wilma" phoneBook 
Nothing

我们实现的这个函数其实模块中已经存在了,就是 Data.List 模块中的 lookup 函数。这个函数的算法需要遍历整个 List,性能稍差,Data.Map 模块提供了更高效的方式(通过树实现),并提供了一组好用的函数。

注意:由于 Data.Map 中的一些函数与 PreludeData.List 模块存在命名冲突,所以我们使用 qualified import

Map 对象

之前我们一直使用关联列表作为键值对,其实还有专门的 Data.Map.Map 对象,他的键必须是可排序的,因为它会按照某顺序将其组织在一棵树中。

在处理键值对时,只要键的型别属于 Ord 型别类,就应该尽量使用Data.Map

fromList

这个函数可以将一个关联列表转换成等价的 Map,其类型声明为

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

测试一下

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

注意:重复的键会被忽略

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

通过 emptyinsertfold,我们可以编写出自己的 fromList

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

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

  • keysmap fst . Map.toList 等价
  • elemsmap 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

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.Set

Data.Set 模块提供了对数学中集合的处理

  • 里面的每个元素都是唯一的,内部由一棵树来组织
  • 必须是可排序的
  • 插入,删除,判断从属关系之类的操作,要比 List 快得多
  • 对一个集合而言,最常见的操作莫过于并集,判断从属或是将集合转为 List

使用的时候同样要限定名称

import qualified Data.Set as Set

假定我们有两个字串,要找出同时存在于两个字串的字符

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"

intersection

取交集

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]

isSubsetOf 和 isProperSubsetOf

判断子集与真子集

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

toList

集合有一常见用途,那就是先 fromList 删掉重复元素后再 toList 转回去。尽管 Data.List 模块的 nub 函数完全可以完成这一工作,但使用集合则会快很多

注意:nub 函数只需 List 中的元素属于 Eq 型别类就行了,而若要使用集合,它必须得属于 Ord 型别类

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

ghci> nub "HEY WHATS CRACKALACKIN"  
"HEY WATSCRKLIN"

可以从中看出,nub 保留了 List 中元素的原有顺序,而 setNub 不。

建立自己的模块

定义模块

文件名和模块名称必须相同吗,在在模块的开头定义模块中的名称

下面,我们在 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

注意:模块中定义了一个辅助函数 rectangleArea。他可以计算矩形面积。因为我们这个模块只与三维图形打交道,因此这个辅助函数不会被导出

使用模块

要使用我们的模块,只需将模块文件 Geometry.hs 文件至于调用进程文件的同一目录之下

import Geometry

组织模块

每个模块都可以含有多个子模块。而子模块还可以有自己的子模块。我们可以把 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

导入的时候就要通过子模块导入了

import Geometry.Sphere

因为各个子模块当中有重名,因此导入的时候可能需要限定

import qualified Geometry.Sphere as Sphere  
import qualified Geometry.Cuboid as Cuboid  
import qualified Geometry.Cube as Cube
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容