[TOC]
## 數據類型入門
在前面的章節中,我們談了一些Haskell內置的類型和類型類。而在本章,我們將學習構造類型和類型類的方法。
我們以已經見識了許多數據類型,如`Bool`、`Int`、`Char`、`Maybe`等等,不過該怎樣構造自己的數據類型呢?好問題,使用data關鍵字是一種方法。我們看看`Bool`在標準庫中的定義:
~~~
data?Bool?=?False?|?True
~~~
**data**表示我們要定義一個新的數據類型。=的左端標明類型的名稱即`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?Circle???
Circle?::?Float?->?Float?->?Float?->?Shape???
ghci>?:t?Rectangle???
Rectangle?::?Float?->?Float?->?Float?->?Float?->?Shape
~~~
Cool,這么說值構造子就跟普通函數并無二致咯,誰想得到?我們寫個函數計算圖形面積:
~~~
surface?::?Shape?->?Float???
surface?(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?10???
314.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?5???
Circle?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?->?Float???
surface?(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?->?Shape???
nudge?(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?10???
Circle?(Point?39.0?44.0)?10.0
~~~
如果不想直接處理Point,我們可以搞個輔助函數(auxilliary function),初始從原點創建圖形,再移動它們。
~~~
baseCircle?::?Float?->?Shape???
baseCircle?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?->?String???
firstName?(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?flavor???
flavor?::?Person?->?String???
ghci>?:t?firstName???
firstName?::?Person?->?String
~~~
還有個好處,就是若派生(deriving)到`Show`類型類,它的顯示是不同的。假如我們有個類型表示一輛車,要包含生產商、型號以及出場年份:
~~~
data?Car?=?Car?String?String?Int?deriving?(Show)
~~~
~~~
ghci>?Car?"Ford"?"Mustang"?1967???
Car?"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](http://fleurer-lee.com/lyah/Int),[Char](http://fleurer-lee.com/lyah/Char)也可以是[String](http://fleurer-lee.com/lyah/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?->?String?
tellCar?(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?->?String???
tellCar?(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?8???
Vector?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}"?::?Person???
Person?{firstName?=?"Michael",?lastName?=?"Diamond",?age?=?43}
~~~
如果我們read的結果會在后面用到參與計算,Haskell就可以推導出是一個Person的行為,不加注釋也是可以的。
~~~
ghci>?read?"Person?{firstName?=\"Michael\",?lastName?=\"Diamond\",?age?=?43}"?==?mikeD???
True
~~~
也可以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`?False???
GT???
ghci>?True?>?False???
True???
ghci>?True??
False
~~~
在Maybe a數據類型中,值構造子Nothing在Just值構造子前面,所以一個`Nothing`總要比`Just something`的值小。即便這個`something`是`100000000`也是如此。
~~~
ghci>?Nothing??
True???
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?|?Sunday????
???????????deriving?(Eq,?Ord,?Show,?Read,?Bounded,?Enum)
~~~
由于它是Show和Read類型類的成員,我們可以將這個類型的值與字符串相互轉換。
~~~
ghci>?Wednesday???
Wednesday???
ghci>?show?Wednesday???
"Wednesday"???
ghci>?read?"Saturday"?::?Day???
Saturday
~~~
由于它是Eq與Ord的成員,因此我們可以拿Day作比較。
~~~
ghci>?Saturday?==?Sunday???
False???
ghci>?Saturday?==?Saturday???
True???
ghci>?Saturday?>?Friday???
True???
ghci>?Monday?`compare`?Wednesday???
LT
~~~
它也是Bounded的成員,因此有最早和最晚的一天。
~~~
ghci>?minBound?::?Day???
Monday???
ghci>?maxBound?::?Day???
Sunday
~~~
它也是Enmu的實例,可以得到前一天和后一天,并且可以對此使用List的區間。
~~~
ghci>?succ?Monday???
Tuesday???
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?=?String???
type?Name?=?String???
type?PhoneBook?=?[(Name,PhoneNumber)]
~~~
Haskell程序員給String加別名是為了讓函數中字符串的表達方式及用途更加明確。
好的,我們實現了一個函數,它可以取一名字和號碼檢查它是否存在于電話本。現在可以給它加一個相當好看明了的類型聲明:
~~~
inPhoneBook?::?Name?->?PhoneNumber?->?PhoneBook?->?Bool???
inPhoneBook?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?20???
Right?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?Map???
data?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?Code???
lockerLookup?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?::?LockerMap???
lockers?=?Map.fromList????
????[(100,(Taken,"ZD39I"))???
????,(101,(Free,"JAH3I"))???
????,(103,(Free,"IQSA9"))???
????,(105,(Free,"QOTSA"))???
????,(109,(Taken,"893JJ"))???
????,(110,(Taken,"99292"))???
????]
~~~
現在從里面lookup某個櫥子號..
~~~
ghci>?lockerLookup?101?lockers???
Right?"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`來表示它的結果,但這樣一來我們就對得不到密碼的原因不得而知了。而在這里,我們的新類型可以告訴我們失敗的原因。