模块
装载模块
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.ListnumUniques :: (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,相当了不起的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] -> Boolsearch 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,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的写法有点像,只是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 elemIndexelemIndex :: (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 -> cf `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 -> Stringencode 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 -> Stringdecode 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)] -> vfindKey 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 vfindKey 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 vfindKey key = foldr (/(k,v) acc -> if key == k then Just v else acc) Nothing
ghci> findKey "penny" phoneBookNote: 通常,使用fold来替代类似的递归函数会更好些。用fold的代码让人一目了然,而看明白递归则得多花点脑子。
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.emptyfromList []
insert取一个键,一个值和一个map做参数,给这个map插入新的键值对,并返回一个新的map。
ghci> Map.emptyfromList []
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 vfromList' = foldr (/(k,v) acc -> Map.insert k v acc) Map.empty
多直白的fold! 从一个空的map开始,然后从右折叠,随着遍历不断地往map中插入新的键值对.
null检查一个map是否为空.
ghci> Map.null Map.emptyTrue
ghci> Map.null $ Map.fromList [(2,3),(5,5)]
False
size返回一个map的大小。
ghci> Map.size Map.empty0
ghci> Map.size $ Map.fromList [(2,4),(3,3),(4,2),(5,4),(6,4)]
5
singleton取一个键值对做参数,并返回一个只含有一个映射的map.
ghci> Map.singleton 3 9fromList [(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 text1ghci> let set2 = Set.fromList text2
ghci> set1
fromList " .?AIRadefhijlmnorstuy"
ghci> set2
fromList " !Tabcdefghilmnorstuvwy"
如你所见,所有的元素都被排了序。而且每个元素都是唯一的。现在我们取它的交集看看它们共同包含的元素:
ghci> Set.intersection set1 set2fromList " adefhilmnorstuy"
使用difference函数可以得到存在于第一个集合但不在第二个集合的元素
ghci> Set.difference set1 set2fromList ".?AIRj"
ghci> Set.difference set2 set1
fromList "!Tbcgvw"
也可以使用union得到两个集合的并集
ghci> Set.union set1 set2fromList " !.?AIRTabcdefghijlmnorstuvwy"
null,size,member,empty,singleton,insert,delete这几个函数就跟你想的差不多啦
ghci> Set.null Set.emptyTrue
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 Sphereimport qualified Geometry.Cuboid as Cuboid
import qualified Geometry.Cube as Cube
然后就可以调用Sphere.area,Sphere.volume,Cuboid.area了,而每个函数都只计算其对应物体的面积和体积.
构造我们自己的类型和类型类
数据类型入门
在前面的章节中,我们谈了一些Haskell内置的类型和类型类。而在本章,我们将学习构造类型和类型类的方法。
我们以已经见识了许多数据类型,如Bool、Int、Char、Maybe等等,不过该怎样构造自己的数据类型呢?好问题,使用data关键字是一种方法。我们看看Bool在标准库中的定义:
data Bool = False | Truedata表示我们要定义一个新的数据类型。=的左端标明类型的名称即Bool,=的右端就是值构造子(Value Constructor),它们明确了该类型可能的值。|读作“或”,所以可以这样阅读该声明:Bool类型的值可以是True或False。类型名和值构造子的首字母必大写。
相似,我们可以假想Int类型的声明:
data Int = -2147483648 | -2147483647 | ... | -1 | 0 | 1 | 2 | ... | 2147483647首位两个值构造子分别表示了Int类型可能的最小值和最大值,这些省略号表示我们省去了中间大段的数字。当然,真实的声明不是这个样子的,这样写只是为了便于理解。
我们想想Haskell中图形的表示方法。表示圆可以用一个元组,如(43.1,55.0,10.4),前两项表示圆心的位置,末项表示半径。听着不错,不过三维向量或其它什么东西也可能是这种形式!更好的方法就是自己构造一个表示图形的类型。假定图形可以是圆(Circle)或长方形(Rectangle):
data Shape = Circle Float Float Float | Rectangle Float Float Float Float这是啥,想想?Circle的值构造子有三个项,都是Float。可见我们在定义值构造子时,可以在后面跟几个类型表示它包含值的类型。在这里,前两项表示圆心的坐标,尾项表示半径。Rectangle的值构造子取四个Float项,前两项表示其左上角的坐标,后两项表示右下角的坐标。
谈到“项”(field),其实应为“参数”(parameters)。值构造子的本质是个函数,可以返回一个类型的值。我们看下这两个值构造子的类型声明:
ghci> :t CircleCircle :: Float -> Float -> Float -> Shape
ghci> :t Rectangle
Rectangle :: Float -> Float -> Float -> Float -> Shape
Cool,这么说值构造子就跟普通函数并无二致咯,谁想得到?我们写个函数计算图形面积:
surface :: Shape -> Floatsurface (Circle _ _ r) = pi * r ^ 2
surface (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1)
值得一提的是,它的类型声明表示了该函数取一个Shape值并返回一个Float值。写Circle -> Float是不可以的,因为Circle并非类型,真正的类型应该是Shape。这与不能写True->False的道理是一样的。再就是,我们使用的模式匹配针对的都是值构造子。之前我们匹配过[]、False或5,它们都是不包含参数的值构造子。
我们只关心圆的半径,因此不需理会表示坐标的前两项:
ghci> surface $ Circle 10 20 10314.15927
ghci> surface $ Rectangle 0 0 100 100
10000.0
Yay,it works!不过我们若尝试输出Circle 10 20到控制台,就会得到一个错误。这是因为Haskell还不知道该类型的字符串表示方法。想想,当我们往控制台输出值的时候,Haskell会先调用show函数得到这个值的字符串表示才会输出。因此要让我们的Shape类型成为Show类型类的成员。可以这样修改:
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show)先不去深究deriving(派生),可以先这样理解:若在data声明的后面加上deriving (Show),那Haskell就会自动将该类型至于Show类型类之中。好了,由于值构造子是个函数,因此我们可以拿它交给map,拿它不全调用,以及普通函数能做的一切。
ghci> Circle 10 20 5Circle 10.0 20.0 5.0
ghci> Rectangle 50 230 60 90
Rectangle 50.0 230.0 60.0 90.0
我们若要取一组不同半径的同心圆,可以这样:
ghci> map (Circle 10 20) [4,5,6,6][Circle 10.0 20.0 4.0,Circle 10.0 20.0 5.0,Circle 10.0 20.0 6.0,Circle 10.0 20.0 6.0]
我们的类型还可以更好。增加加一个表示二维空间中点的类型,可以让我们的Shape更加容易理解:
data Point = Point Float Float deriving (Show)data Shape = Circle Point Float | Rectangle Point Point deriving (Show)
注意下Point的定义,它的类型与值构造子用了相同的名字。没啥特殊含义,实际上,在一个类型含有唯一值构造子时这种重名是很常见的。好的,如今我们的Circle含有两个项,一个是Point类型,一个是Float类型,好作区分。Rectangle也是同样,我们得修改surface函数以适应类型定义的变动。
surface :: Shape -> Floatsurface (Circle _ r) = pi * r ^ 2
surface (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1)
唯一需要修改的地方就是模式。在Circle的模式中,我们无视了整个Point。而在Rectangle的模式中,我们用了一个嵌套的模式来取得Point中的项。若出于某原因而需要整个Point,那么直接匹配就是了。
ghci> surface (Rectangle (Point 0 0) (Point 100 100))10000.0
ghci> surface (Circle (Point 0 0) 24)
1809.5574
表示移动一个图形的函数该怎么写? 它应当取一个Shape和表示位移的两个数,返回一个位于新位置的图形。
nudge :: Shape -> Float -> Float -> Shapenudge (Circle (Point x y) r) a b = Circle (Point (x+a) (y+b)) r
nudge (Rectangle (Point x1 y1) (Point x2 y2)) a b = Rectangle (Point (x1+a) (y1+b)) (Point (x2+a) (y2+b))
很直白,我们给这一Shape的点加上位移的量。
ghci> nudge (Circle (Point 34 34) 10) 5 10Circle (Point 39.0 44.0) 10.0
如果不想直接处理Point,我们可以搞个辅助函数(auxilliary function),初始从原点创建图形,再移动它们。
baseCircle :: Float -> ShapebaseCircle r = Circle (Point 0 0) r
baseRect :: Float -> Float -> Shape
baseRect width height = Rectangle (Point 0 0) (Point width height) ghci> nudge (baseRect 40 100) 60 23
Rectangle (Point 60.0 23.0) (Point 100.0 123.0)
毫无疑问,你可以把你的数据类型导出到模块中。只要把你的类型与要导出的函数写到一起就是了。再在后面跟个括号,列出要导出的值构造子,用逗号隔开。如要导出所有的值构造子,那就写个..。
若要将这里定义的所有函数和类型都导出到一个模块中,可以这样:
module Shapes( Point(..)
, Shape(..)
, surface
, nudge
, baseCircle
, baseRect
) where
一个Shape (..),我们就导出了Shape的所有值构造子。这一来无论谁导入我们的模块,都可以用Rectangle和Circle值构造子来构造Shape了。这与写Shape(Rectangle,Circle)等价。
我们可以选择不导出任何Shape的值构造子,这一来使用我们模块的人就只能用辅助函数baseCircle和baseRect来得到Shape了。Data.Map就是这一套,没有Map.Map [(1,2),(3,4)],因为它没有导出任何一个值构造子。但你可以用,像Map.fromList这样的辅助函数得到map。应该记住,值构造子只是函数而已,如果不导出它们,就拒绝了使用我们模块的人调用它们。但可以使用其他返回该类型的函数,来取得这一类型的值。
不导出数据类型的值构造子隐藏了他们的内部实现,令类型的抽象度更高。同时,我们模块的使用者也就无法使用该值构造子进行模式匹配了。
Record Syntax
OK,我们需要一个数据类型来描述一个人,得包含他的姓、名、年龄、身高、体重、电话号码以及最爱的冰激淋。我不知你的想法,不过我觉得要了解一个人,这些资料就够了。就这样,实现出来!
data Person = Person String String Int Float String String deriving (Show)O~Kay,第一项是名,第二项是姓,第三项是年龄,等等。我们造一个人:
ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"ghci> guy
Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"
貌似很酷,就是难读了点儿。弄个函数得人的某项资料又该如何?如姓的函数,名的函数,等等。好吧,我们只能这样:
firstName :: Person -> StringfirstName (Person firstname _ _ _ _ _) = firstname
lastName :: Person -> String
lastName (Person _ lastname _ _ _ _) = lastname
age :: Person -> Int
age (Person _ _ age _ _ _) = age
height :: Person -> Float
height (Person _ _ _ height _ _) = height
phoneNumber :: Person -> String
phoneNumber (Person _ _ _ _ number _) = number
flavor :: Person -> String
flavor (Person _ _ _ _ _ flavor) = flavor
唔,我可不愿写这样的代码!虽然it works,但也太无聊了哇。
ghci> let guy = Person "Buddy" "Finklestein" 43 184.2 "526-2928" "Chocolate"ghci> firstName guy
"Buddy"
ghci> height guy
184.2
ghci> flavor guy
"Chocolate"
你可能会说,一定有更好的方法!呃,抱歉,没有。
开个玩笑,其实有的,哈哈哈~Haskell的发明者都是天才,早就料到了此类情形。他们引入了一个特殊的类型,也就是刚才提到的更好的方法--Record Syntax。
data Person = Person { firstName :: String, lastName :: String
, age :: Int
, height :: Float
, phoneNumber :: String
, flavor :: String
} deriving (Show)
与原先让那些项一个挨一个的空格隔开不同,这里用了花括号{}。先写出项的名字,如firstName,后跟两个冒号(也叫Raamayim Nekudotayim,哈哈~(译者不知道什么意思~囧)),标明其类型,返回的数据类型仍与以前相同。这样的好处就是,可以用函数从中直接按项取值。通过Record Syntax,haskell就自动生成了这些函数:firstName,lastName,age,height,phoneNumber和flavor。
ghci> :t flavorflavor :: Person -> String
ghci> :t firstName
firstName :: Person -> String
还有个好处,就是若派生(deriving)到Show类型类,它的显示是不同的。假如我们有个类型表示一辆车,要包含生产商、型号以及出场年份:
data Car = Car String String Int deriving (Show) ghci> Car "Ford" "Mustang" 1967Car "Ford" "Mustang" 1967
若用Record Syntax,就可以得到像这样的新车:
data Car = Car {company :: String, model :: String, year :: Int} deriving (Show) ghci> Car {company="Ford", model="Mustang", year=1967}Car {company = "Ford", model = "Mustang", year = 1967}
这一来在造车时我们就不必关心各项的顺序了。
表示三维向量之类简单数据,Vector = Vector Int Int Int就足够明白了。但一个值构造子中若含有很多个项且不易区分,如一个人或者一辆车啥的,就应该使用Record Syntax。
类型参数
值构造子可以取几个参数产生一个新值,如Car的构造子是取三个参数返回一个Car。与之相似,类型构造子可以取类型作参数,产生新的类型。这咋一听貌似有点深奥,不过实际上并不复杂。如果你对C++的模板有了解,就会看到很多相似的地方。我们看一个熟悉的类型,好对类型参数有个大致印象:
data Maybe a = Nothing | Just a这里的a就是个类型参数。也正因为有了它,Maybe就成为了一个类型构造子。在它的值不是Nothing时,它的类型构造子可以搞出Maybe Int,Maybe String等等诸多类型。但只一个Maybe是不行的,因为它不是类型,而是类型构造子。要成为真正的类型,必须得把它需要的类型参数全部填满。
所以,如果拿Char作参数交给Maybe,就可以得到一个Maybe Char的类型。如,Just 'a'的类型就是Maybe Char。
你可能并未察觉,在遇见Maybe之前我们早就接触到类型参数了。它便是List类型。这里面有点语法糖,List类型实际上就是取一个参数来生成一个特定类型,这类型可以是Int,Char也可以是String,但不会跟在[]的后面。
把玩一下Maybe!
ghci> Just "Haha"Just "Haha"
ghci> Just 84
Just 84
ghci> :t Just "Haha"
Just "Haha" :: Maybe [Char]
ghci> :t Just 84
Just 84 :: (Num t) => Maybe t
ghci> :t Nothing
Nothing :: Maybe a
ghci> Just 10 :: Maybe Double
Just 10.0
类型参数很实用。有了它,我们就可以按照我们的需要构造出不同的类型。若执行:t Just "Haha",类型推导引擎就会认出它是个Maybe [Char],由于Just a里的a是个字符串,那么Maybe a里的a一定也是个字符串。
注意下,Nothing的类型为Maybe a。它是多态的,若有函数取Maybe Int类型的参数,就一概可以传给它一个Nothing,因为Nothing中不包含任何值。Maybe a类型可以有Maybe Int的行为,正如5可以是Int也可以是Double。与之相似,空List的类型是[a],可以与一切List打交道。因此,我们可以[1,2,3]++[],也可以["ha","ha,","ha"]++[]。
类型参数有很多好处,但前提是用对了地方才行。一般都是不关心类型里面的内容,如Maybe a。一个类型的行为若有点像是容器,那么使用类型参数会是个不错的选择。我们完全可以把我们的Car类型从
data Car = Car { company :: String, model :: String
, year :: Int
} deriving (Show)
改成:
data Car a b c = Car { company :: a, model :: b
, year :: c
} deriving (Show)
但是,这样我们又得到了什么好处?回答很可能是,一无所得。因为我们只定义了处理Car String String Int类型的函数,像以前,我们还可以弄个简单函数来描述车的属性。
tellCar :: Car -> StringtellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y ghci> let stang = Car {company="Ford", model="Mustang", year=1967}
ghci> tellCar stang "This Ford Mustang was made in 1967"
可爱的小函数!它的类型声明得很漂亮,而且工作良好。好,如果改成Car a b c又会怎样?
tellCar :: (Show a) => Car String String a -> StringtellCar (Car {company = c, model = m, year = y}) = "This " ++ c ++ " " ++ m ++ " was made in " ++ show y
我们只能强制性地给这个函数安一个(Show a) => Car String String a 的类型约束。看得出来,这要繁复得多。而唯一的好处貌似就是,我们可以使用Show类型类的实例来作a的类型。
ghci> tellCar (Car "Ford" "Mustang" 1967)"This Ford Mustang was made in 1967"
ghci> tellCar (Car "Ford" "Mustang" "nineteen sixty seven")
"This Ford Mustang was made in /"nineteen sixty seven/""
ghci> :t Car "Ford" "Mustang" 1967
Car "Ford" "Mustang" 1967 :: (Num t) => Car [Char] [Char] t
ghci> :t Car "Ford" "Mustang" "nineteen sixty seven"
Car "Ford" "Mustang" "nineteen sixty seven" :: Car [Char] [Char] [Char]
其实在现实生活中,使用Car String String Int在大多数情况下已经满够了。所以给Car类型加类型参数貌似并没有什么必要。通常我们都是都是在一个类型中包含的类型并不影响它的行为时才引入类型参数。一组什么东西组成的List就是一个List,它不关心里面东西的类型是啥,然而总是工作良好。若取一组数字的和,我们可以在后面的函数体中明确是一组数字的List。Maybe与之相似,它表示可以有什么东西可以没有,而不必关心这东西是啥。
我们之前还遇见过一个类型参数的应用,就是Data.Map中的Map k v。k表示Map中键的类型,v表示值的类型。这是个好例子,map中类型参数的使用允许我们能够用一个类型索引另一个类型,只要键的类型在Ord类型类就行。如果叫我们自己定义一个map类型,可以在data声明中加上一个类型类的约束。
data (Ord k) => Map k v = ...然而haskell中有一个严格的约定,那就是永远不要在data声明中添加类型约束。为啥?嗯,因为这样没好处,反而得写更多不必要的类型约束。Map k v要是有Ord k的约束,那就相当于假定每个map的相关函数都认为k是可排序的。若不给数据类型加约束,我们就不必给那些不关心键是否可排序的函数另加约束了。这类函数的一个例子就是toList,它只是把一个map转换为关联List罢了,类型声明为toList :: Map k v -> [(k, v)]。要是加上类型约束,就只能是toList :: (Ord k) =>Map k a -> [(k,v)],明显没必要嘛。
所以说,永远不要在data声明中加类型约束---即便看起来没问题。免得在函数声明中写出过多无畏的类型约束。
我们实现个表示三维向量的类型,再给它加几个处理函数。我么那就给它个类型参数,虽然大多数情况都是数值型,不过这一来它就支持了多种数值类型。
data Vector a = Vector a a a deriving (Show)vplus :: (Num t) => Vector t -> Vector t -> Vector t
(Vector i j k) `vplus` (Vector l m n) = Vector (i+l) (j+m) (k+n)
vectMult :: (Num t) => Vector t -> t -> Vector t
(Vector i j k) `vectMult` m = Vector (i*m) (j*m) (k*m)
scalarMult :: (Num t) => Vector t -> Vector t -> t
(Vector i j k) `scalarMult` (Vector l m n) = i*l + j*m + k*n
vplus用来相加两个向量,即将其所有对应的项相加。scalarMult用来求两个向量的标量积,vectMult求一个向量和一个标量的积。这些函数可以处理Vector Int,Vector Integer,Vector Float等等类型,只要Vector a里的这个a在Num类型类中就行。同样,如果你看下这些函数的类型声明就会发现,它们只能处理相同类型的向量,其中包含的数字类型必须与另一个向量一致。注意,我们并没有在data声明中添加Num的类约束。反正无论怎么着都是给函数加约束。
再度重申,类型构造子和值构造子的区分是相当重要的。在声明数据类型时,等号=左端的那个是类型构造子,右端的(中间可能有|分隔)都是值构造子。拿Vector t t t -> Vector t t t -> t作函数的类型就会产生一个错误,因为在类型声明中只能写类型,而Vector的类型构造子只有个参数,它的值构造子才是有三个。我们就慢慢耍:
ghci> Vector 3 5 8 `vplus` Vector 9 2 8Vector 12 7 16
ghci> Vector 3 5 8 `vplus` Vector 9 2 8 `vplus` Vector 0 2 3
Vector 12 9 19
ghci> Vector 3 9 7 `vectMult` 10
Vector 30 90 70
ghci> Vector 4 9 5 `scalarMult` Vector 9.0 2.0 4.0
74.0
ghci> Vector 2 9 3 `vectMult` (Vector 4 9 5 `scalarMult` Vector 9 2 4)
Vector 148 666 222
派生实例
在typeclass 101那节里面,我们了解了typeclass的基础内容。里面提到,类型类就是定义了某些行为的接口。例如,Int类型是Eq类型类的一个实例,Eq类就定义了判定相等性的行为。Int值可以判断相等性,所以Int就是Eq类型类的成员。它的真正威力体现在作为Eq接口的函数中,即==和/=。只要一个类型是Eq类型类的成员,我们就可以使用==函数来处理这一类型。这便是为何4==4和"foo"/="bar"这样的表达式都需要作类型检查。
我们也曾提到,人们很容易把类型类与Java,python,C++等语言的类混淆。很多人对此都倍感不解,在原先那些语言中,类就像是蓝图,我们可以根据它来创造对象、保存状态并执行操作。而类型类更像是接口,我们不是靠它构造数据,而是给既有的数据类型描述行为。什么东西若可以判定相等性,我们就可以让它成为Eq类型类的实例。什么东西若可以比较大小,那就可以让它成为Ord类型类的实例。
在下一节,我们将看一下如何手工实现类型类中定义函数来构造实例。现在呢,我们先了解下Haskell是如何自动生成这几个类型类的实例,Eq,Ord,Enum,Bounded,Show,Read。只要我们在构造类型时在后面加个deriving(派生)关键字,Haskell就可以自动地给我们的类型加上这些行为。
看这个数据类型:
data Person = Person { firstName :: String, lastName :: String
, age :: Int
}
这描述了一个人。我们先假定世界上没有重名重姓又同龄的人存在,好,假如有两个record,有没有可能是描述同一个人呢?当然可能,我么可以判定姓名年龄的相等性,来判断它俩是否相等。这一来,让这个类型成为Eq的成员就很靠谱了。直接derive这个实例:
data Person = Person { firstName :: String, lastName :: String
, age :: Int
} deriving (Eq)
在一个类型派生为Eq的实例后,就可以直接使用==或/=来判断它们的相等性了。Haskell会先看下这两个值的值构造子是否一致(这里只是单值构造子),再用==来检查其中的所有数据(必须都是Eq的成员)是否一致。在这里只有String和Int,所以是没有问题的。测试下我们的Eq实例:
ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}ghci> let adRock = Person {firstName = "Adam", lastName = "Horovitz", age = 41}
ghci> let mca = Person {firstName = "Adam", lastName = "Yauch", age = 44}
ghci> mca == adRock
False
ghci> mikeD == adRock
False
ghci> mikeD == mikeD
True
ghci> mikeD == Person {firstName = "Michael", lastName = "Diamond", age = 43}
True
自然,Person如今已经成为了Eq的成员,我们就可以将其应用于所有在类型声明中用到Eq类约束的函数了,如elem。
ghci> let beastieBoys = [mca, adRock, mikeD]ghci> mikeD `elem` beastieBoys
True
Show和Read类型类处理可与字符串相互转换的东西。同Eq相似,如果一个类型的构造子含有参数,那所有参数的类型必须都得属于Show或Read才能让该类型成为其实例。就让我们的Person也成为Read和Show的一员吧。
data Person = Person { firstName :: String, lastName :: String
, age :: Int
} deriving (Eq, Show, Read)
然后就可以输出一个Person到控制台了。
ghci> let mikeD = Person {firstName = "Michael", lastName = "Diamond", age = 43}ghci> mikeD
Person {firstName = "Michael", lastName = "Diamond", age = 43}
ghci> "mikeD is: " ++ show mikeD
"mikeD is: Person {firstName = /"Michael/", lastName = /"Diamond/", age = 43}"
如果我们还没让Person类型作为Show的成员就尝试输出它,haskell就会向我们抱怨,说它不知道该怎么把它表示成一个字符串。不过现在既然已经派生成为了Show的一个实例,它就知道了。
Read几乎就是与Show相对的类型类,show是将一个值转换成字符串,而read则是将一个字符串转成某类型的值。还记得,使用read函数时我们必须得用类型注释注明想要的类型,否则haskell就不会知道如何转换。
ghci> read "Person {firstName =/"Michael/", lastName =/"Diamond/", age = 43}" :: PersonPerson {firstName = "Michael", lastName = "Diamond", age = 43}
如果我们read的结果会在后面用到参与计算,Haskell就可以推导出是一个Person的行为,不加注释也是可以的。
ghci> read "Person {firstName =/"Michael/", lastName =/"Diamond/", age = 43}" == mikeDTrue
也可以read带参数的类型,但必须填满所有的参数。因此read "Just 't'" :: Maybe a是不可以的,read "Just 't'" :: Maybe Char才对。
很容易想象Ord类派生实例的行为。首先,判断两个值构造子是否一致,如果是,再判断它们的参数,前提是它们的参数都得是Ord的实例。Bool类型可以有两种值,False和True。为了了解在比较中程序的行为,我们可以这样想象:
data Bool = False | True deriving (Ord)由于值构造子False安排在True的前面,我们可以认为True比False大。
ghci> True `compare` FalseGT
ghci> True > False
True
ghci> True
False
在Maybe a数据类型中,值构造子Nothing在Just值构造子前面,所以一个Nothing总要比Just something的值小。即便这个something是100000000也是如此。
ghci> NothingTrue
ghci> Nothing > Just (-49999)
False
ghci> Just 3 `compare` Just 2
GT
ghci> Just 100 > Just 50
True
不过类似Just (3), Just(2)之类的代码是不可以的。因为(3)和(2)都是函数,而函数不是Ord类的成员。
作枚举,使用数字类型就能轻易做到。不过使用Enmu和Bounded类型类会更好,看下这个类型:
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday所有的值构造子都是nullary的(也就是没有参数),每个东西都有前置子和后继子,我们可以让它成为Enmu类型类的成员。同样,每个东西都有可能的最小值和最大值,我们也可以让它成为Bounded类型类的成员。在这里,我们就同时将它搞成其它可派生类型类的实例。再看看我们能拿它做啥:
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sundayderiving (Eq, Ord, Show, Read, Bounded, Enum)
由于它是Show和Read类型类的成员,我们可以将这个类型的值与字符串相互转换。
ghci> WednesdayWednesday
ghci> show Wednesday
"Wednesday"
ghci> read "Saturday" :: Day
Saturday
由于它是Eq与Ord的成员,因此我们可以拿Day作比较。
ghci> Saturday == SundayFalse
ghci> Saturday == Saturday
True
ghci> Saturday > Friday
True
ghci> Monday `compare` Wednesday
LT
它也是Bounded的成员,因此有最早和最晚的一天。
ghci> minBound :: DayMonday
ghci> maxBound :: Day
Sunday
它也是Enmu的实例,可以得到前一天和后一天,并且可以对此使用List的区间。
ghci> succ MondayTuesday
ghci> pred Saturday
Friday
ghci> [Thursday .. Sunday]
[Thursday,Friday,Saturday,Sunday]
ghci> [minBound .. maxBound] :: [Day]
[Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday]
那是相当的棒。
类型别名
在前面我们提到在写类型名的时候,[Char]和String等价,可以互换。这就是由类型别名实现的。类型别名实际上什么也没做,只是给类型提供了不同的名字,让我们的代码更容易理解。这就是[Char]的别名String的由来。
type String = [Char]我们已经介绍过了type关键字,这个关键字有一定误导性,它并不是用来创造新类(这是data关键字做的事情),而是给一个既有类型提供一个别名。
如果我们随便搞个函数toUpperString或其他什么名字,将一个字符串变成大写,可以用这样的类型声明toUpperString :: [Char] -> [Char], 也可以这样toUpperString :: String -> String,二者在本质上是完全相同的。后者要更易读些。
在前面Data.Map那部分,我们用了一个关联List来表示phoneBook,之后才改成的Map。我们已经发现了,一个关联List就是一组键值对组成的List。再看下我们phoneBook的样子:
phoneBook :: [(String,String)]phoneBook =
[("betty","555-2938")
,("bonnie","452-2928")
,("patsy","493-2928")
,("lucille","205-2928")
,("wendy","939-8282")
,("penny","853-2492")
]
可以看出,phoneBook的类型就是[(String,String)],这表示一个关联List仅是String到String的映射关系。我们就弄个类型别名,好让它类型声明中能够表达更多信息。
type PhoneBook = [(String,String)]现在我们phoneBook的类型声明就可以是phoneBook :: PhoneBook了。再给字符串加上别名:
type PhoneNumber = Stringtype Name = String
type PhoneBook = [(Name,PhoneNumber)]
Haskell程序员给String加别名是为了让函数中字符串的表达方式及用途更加明确。
好的,我们实现了一个函数,它可以取一名字和号码检查它是否存在于电话本。现在可以给它加一个相当好看明了的类型声明:
inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> BoolinPhoneBook name pnumber pbook = (name,pnumber) `elem` pbook
如果不用类型别名,我们函数的类型声明就只能是String -> String -> [(String ,String)] -> Bool了。在这里使用类型别名是为了让类型声明更加易读,但你也不必拘泥于它。引入类型别名的动机既非单纯表示我们函数中的既有类型,也不是为了替换掉那些重复率高的长名字类型(如[(String,String)]),而是为了让类型对事物的描述更加明确。
类型别名也是可以有参数的,如果你想搞个类型来表示关联List,但依然要它保持通用,好让它可以使用任意类型作key和value,我们可以这样:
type AssocList k v = [(k,v)]好的,现在一个从关联List中按键索值的函数类型可以定义为(Eq k) => k -> AssocList k v -> Maybe v. AssocList i。AssocList是个取两个类型做参数生成一个具体类型的类型构造子,如Assoc Int String等等。
Fronzie说:Hey!当我提到具体类型,那我就是说它是完全调用的,就像Map Int String。要不就是多态函数中的[a]或(Ord a) => Maybe a之类。有时我和孩子们会说“Maybe类型”,但我们的意思并不是按字面来,傻瓜都知道Maybe是类型构造子嘛。只要用一个明确的类型调用Maybe,如Maybe String可得一个具体类型。你知道,只有具体类型才可以储存值。
我们可以用不全调用来得到新的函数,同样也可以使用不全调用得到新的类型构造子。同函数一样,用不全的类型参数调用类型构造子就可以得到一个不全调用的类型构造子,如果我们要一个表示从整数到某东西间映射关系的类型,我们可以这样:
type IntMap v = Map Int v也可以这样:
type IntMap = Map Int无论怎样,IntMap的类型构造子都是取一个参数,而它就是这整数指向的类型。
Oh yeah,如果要你去实现它,很可能会用个qualified import来导入Data.Map。这时,类型构造子前面必须得加上模块名。所以应该写个type IntMap = Map.Map Int
你得保证真正弄明白了类型构造子和值构造子的区别。我们有了个叫IntMap或者AssocList的别名并不意味着我们可以执行类似AssocList [(1,2),(4,5),(7,9)]的代码,而是可以用不同的名字来表示原先的List,就像[(1,2),(4,5),(7,9)] :: AssocList Int Int让它里面的类型都是Int。而像处理普通的二元组构成的那种List处理它也是可以的。类型别名(类型依然不变),只可以在Haskell的类型部分中使用,像定义新类型或类型声明或类型注释中跟在::后面的部分。
另一个很酷的二参类型就是Either a b了,它大约是这样定义的:
data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show)它有两个值构造子。如果用了Left,那它内容的类型就是a;用了Right,那它内容的类型就是b。我们可以用它来将可能是两种类型的值封装起来,从里面取值时就同时提供Left和Right的模式匹配。
ghci> Right 20Right 20
ghci> Left "w00t"
Left "w00t"
ghci> :t Right 'a'
Right 'a' :: Either a Char
ghci> :t Left True
Left True :: Either Bool b
到现在为止,Maybe是最常见的表示可能失败的计算的类型了。但有时Maybe也并不是十分的好用,因为Nothing中包含的信息还是太少。要是我们不关心函数失败的原因,它还是不错的。就像Data.Map的lookup只有在搜寻的项不在map时才会失败,对此我们一清二楚。但我们若想知道函数失败的原因,那还得使用Either a b,用a来表示可能的错误的类型,用b来表示一个成功运算的类型。从现在开始,错误一律用Left值构造子,而结果一律用Right。
一个例子:有个学校提供了不少壁橱,好给学生们地方放他们的Gun'N'Rose海报。每个壁橱都有个密码,哪个学生想用个壁橱,就告诉管理员壁橱的号码,管理员就会告诉他壁橱的密码。但如果这个壁橱已经让别人用了,管理员就不能告诉他密码了,得换一个壁橱。我们就用Data.Map的一个map来表示这些壁橱,把一个号码映射到一个表示壁橱占用情况及密码的二元组里。
import qualified Data.Map as Mapdata LockerState = Taken | Free deriving (Show, Eq)
type Code = String
type LockerMap = Map.Map Int (LockerState, Code)
很简单,我们引入了一个新的类型来表示壁橱的占用情况。并为壁橱密码及按号码找壁橱的map分别设置了一个别名。好,现在我们实现这个按号码找壁橱的函数,就用Either String Code类型表示我们的结果,因为lookup可能会以两种原因失败。厨子已经让别人用了或者压根就没有这个橱子。如果lookup失败,就用字符串表明失败的原因。
lockerLookup :: Int -> LockerMap -> Either String CodelockerLookup lockerNumber map =
case Map.lookup lockerNumber map of
Nothing -> Left $ "Locker number " ++ show lockerNumber ++ " doesn't exist!"
Just (state, code) -> if state /= Taken
then Right code
else Left $ "Locker " ++ show lockerNumber ++ " is already taken!"
我们在这里个map中执行一次普通的lookup,如果得到一个Nothing,就返回一个Left String的值,告诉他压根就没这个号码的橱子。如果找到了,就再检查下,看这橱子是不是已经让别人用了,如果是,就返回个Left String说它已经让别人用了。否则就返回个Right Code的值,通过它来告诉学生壁橱的密码。它实际上就是个Right String,我们引入了个类型别名让它这类型声明更好看。
如下是个map的例子:
lockers :: LockerMaplockers = Map.fromList
[(100,(Taken,"ZD39I"))
,(101,(Free,"JAH3I"))
,(103,(Free,"IQSA9"))
,(105,(Free,"QOTSA"))
,(109,(Taken,"893JJ"))
,(110,(Taken,"99292"))
]
现在从里面lookup某个橱子号..
ghci> lockerLookup 101 lockersRight "JAH3I"
ghci> lockerLookup 100 lockers
Left "Locker 100 is already taken!"
ghci> lockerLookup 102 lockers
Left "Locker number 102 doesn't exist!"
ghci> lockerLookup 110 lockers
Left "Locker 110 is already taken!"
ghci> lockerLookup 105 lockers
Right "QOTSA"
我们完全可以用Maybe a来表示它的结果,但这样一来我们就对得不到密码的原因不得而知了。而在这里,我们的新类型可以告诉我们失败的原因。