http://wiki.jikexueyuan.com/project/haskell-guide/images/cow.png" alt="" />
之前我們有說(shuō)過(guò) Haskell 是 Static Type,這表示在編譯時(shí)期每個(gè)表達(dá)式的型別都已經(jīng)確定下來(lái),這提高了程式碼的安全性。若程式碼中有讓布林值與數(shù)字相除的動(dòng)作,就不會(huì)通過(guò)編譯。這樣的好處就是與其讓程序在運(yùn)行時(shí)崩潰,不如在編譯時(shí)就找出可能的錯(cuò)誤。Haskell 中所有東西都有型別,因此在編譯的時(shí)候編譯器可以做到很多事情。
與 Java 和 Pascal 不同,Haskell 支持型別推導(dǎo)。寫下一個(gè)數(shù)字,你就沒(méi)必要另告訴 Haskell 說(shuō)"它是個(gè)數(shù)字",它自己能推導(dǎo)出來(lái)。這樣我們就不必在每個(gè)函數(shù)或表達(dá)式上都標(biāo)明其型別了。在前面我們只簡(jiǎn)單涉及一下 Haskell 的型別方面的知識(shí),但是理解這一型別系統(tǒng)對(duì)于 Haskell 的學(xué)習(xí)是至關(guān)重要的。
型別是每個(gè)表達(dá)式都有的某種標(biāo)簽,它標(biāo)明了這一表達(dá)式所屬的范疇。例如,表達(dá)式 True 是 boolean 型,"hello"是個(gè)字串,等等。
可以使用 ghci 來(lái)檢測(cè)表達(dá)式的型別。使用 :t 命令后跟任何可用的表達(dá)式,即可得到該表達(dá)式的型別,先試一下:
ghci> :t 'a'
'a' :: Char
ghci> :t True
True :: Bool
ghci> :t "HELLO!"
"HELLO!" :: [Char]
ghci> :t (True, 'a')
(True, 'a') :: (Bool, Char)
ghci> :t 4 == 5
4 == 5 :: Bool
http://wiki.jikexueyuan.com/project/haskell-guide/images/bomb.png" alt="" />
可以看出,:t 命令處理一個(gè)表達(dá)式的輸出結(jié)果為表達(dá)式后跟 :: 及其型別,:: 讀作"它的型別為"。凡是明確的型別,其首字母必為大寫。'a', 如它的樣子,是 Char 型別,易知是個(gè)字元 (character)。True 是 Bool 型別,也靠譜。不過(guò)這又是啥,檢測(cè) "hello" 得一個(gè) [Char] 這方括號(hào)表示一個(gè) List,所以我們可以將其讀作"一組字元的 List"。而與 List 不同,每個(gè) Tuple 都是獨(dú)立的型別,于是 (True,'a') 的型別是 (Bool,Char),而 ('a','b','c') 的型別為 (Char,Char,Char)。4==5 一定回傳 False,所以它的型別為 Bool。
同樣,函數(shù)也有型別。編寫函數(shù)時(shí),給它一個(gè)明確的型別聲明是個(gè)好習(xí)慣,比較短的函數(shù)就不用多此一舉了。還記得前面那個(gè)過(guò)濾大寫字母的 List Comprehension 嗎?給它加上型別聲明便是這個(gè)樣子:
removeNonUppercase :: [Char] -> [Char]
removeNonUppercase st = [ c | c <- st, c `elem` ['A'..'Z']]
removeNonUppercase 的型別為 [Char]->[Char],從它的參數(shù)和回傳值的型別上可以看出,它將一個(gè)字串映射為另一個(gè)字串。[Char] 與 String 是等價(jià)的,但使用 String 會(huì)更清晰:removeNonUppercase :: String -> String。編譯器會(huì)自動(dòng)檢測(cè)出它的型別,我們還是標(biāo)明了它的型別聲明。要是多個(gè)參數(shù)的函數(shù)該怎樣?如下便是一個(gè)將三個(gè)整數(shù)相加的簡(jiǎn)單函數(shù)。
addThree :: Int -> Int -> Int -> Int
addThree x y z = x + y + z
參數(shù)之間由 -> 分隔,而與回傳值之間并無(wú)特殊差異?;貍髦凳亲詈笠豁?xiàng),參數(shù)就是前三項(xiàng)。稍后,我們將講解為何只用 -> 而不是 Int,Int,Int->Int 之類"更好看"的方式來(lái)分隔參數(shù)。
如果你打算給你編寫的函數(shù)加上個(gè)型別聲明卻拿不準(zhǔn)它的型別是啥,只要先不寫型別聲明,把函數(shù)體寫出來(lái),再使用 :t 命令測(cè)一下即可。函數(shù)也是表達(dá)式,所以 :t 對(duì)函數(shù)也是同樣可用的。
如下是幾個(gè)常見(jiàn)的型別:
Int 表示整數(shù)。7 可以是 Int,但 7.2 不可以。Int 是有界的,也就是說(shuō)它由上限和下限。對(duì) 32 位的機(jī)器而言,上限一般是 2147483647,下限是 -2147483648。
Integer 表示...厄...也是整數(shù),但它是無(wú)界的。這就意味著可以用它存放非常非常大的數(shù),我是說(shuō)非常大。它的效率不如 Int 高。
factorial :: Integer -> Integer
factorial n = product [1..n]
ghci> factorial 50
30414093201713378043612608166064768844377641568960512000000000000
Float 表示單精度的浮點(diǎn)數(shù)。
circumference :: Float -> Float
circumference r = 2 * pi * r
ghci> circumference 4.0
25.132742
Double 表示雙精度的浮點(diǎn)數(shù)。
circumference' :: Double -> Double
circumference' r = 2 * pi * r
ghci> circumference' 4.0
25.132741228718345
Bool 表示布林值,它只有兩種值:True 和 False。
Char 表示一個(gè)字元。一個(gè)字元由單引號(hào)括起,一組字元的 List 即為字串。
Tuple 的型別取決于它的長(zhǎng)度及其中項(xiàng)的型別。注意,空 Tuple 同樣也是個(gè)型別,它只有一種值:()。
你覺(jué)得 head 函數(shù)的型別是啥?它可以取任意型別的 List 的首項(xiàng),是怎么做到的呢?我們查一下!
ghci> :t head
head :: [a] -> a
http://wiki.jikexueyuan.com/project/haskell-guide/images/box.png" alt="" />
嗯! a 是啥?型別嗎?想想我們?cè)谇懊嬲f(shuō)過(guò),凡是型別其首字母必大寫,所以它不會(huì)是個(gè)型別。它是個(gè)型別變數(shù),意味著 a 可以是任意的型別。這一點(diǎn)與其他語(yǔ)言中的泛型 (generic) 很相似,但在 Haskell 中要更為強(qiáng)大。它可以讓我們輕而易舉地寫出型別無(wú)關(guān)的函數(shù)。使用到型別變數(shù)的函數(shù)被稱作"多態(tài)函數(shù) ",head 函數(shù)的型別聲明里標(biāo)明了它可以取任意型別的 List 并回傳其中的第一個(gè)元素。
在命名上,型別變數(shù)使用多個(gè)字元是合法的,不過(guò)約定俗成,通常都是使用單個(gè)字元,如 a, b ,c ,d...
還記得 fst?我們查一下它的型別:
ghci> :t fst
fst :: (a, b) -> a
可以看到fst取一個(gè)包含兩個(gè)型別的 Tuple 作參數(shù),并以第一個(gè)項(xiàng)的型別作為回傳值。這便是 fst 可以處理一個(gè)含有兩種型別項(xiàng)的 pair 的原因。注意,a 和 b 是不同的型別變數(shù),但它們不一定非得是不同的型別,它只是標(biāo)明了首項(xiàng)的型別與回傳值的型別相同。
http://wiki.jikexueyuan.com/project/haskell-guide/images/classes.png" alt="" />
型別定義行為的介面,如果一個(gè)型別屬于某 Typeclass,那它必實(shí)現(xiàn)了該 Typeclass 所描述的行為。很多從 OOP 走過(guò)來(lái)的人們往往會(huì)把 Typeclass 當(dāng)成物件導(dǎo)向語(yǔ)言中的 class 而感到疑惑,厄,它們不是一回事。易于理解起見(jiàn),你可以把它看做是 Java 的 interface。
== 函數(shù)的型別聲明是怎樣的?
ghci> :t (==)
(==) :: (Eq a) => a -> a -> Bool
*Note*: 判斷相等的==運(yùn)算子是函數(shù),``+-*/``之類的運(yùn)算子也是同樣。在預(yù)設(shè)條件下,它們多為中綴函數(shù)。若要檢查它的型別,就必須得用括號(hào)括起使之作為另一個(gè)函數(shù),或者說(shuō)以首碼函數(shù)的形式呼叫它。
有意思。在這里我們見(jiàn)到個(gè)新東西:=> 符號(hào)。它左邊的部分叫做型別約束。我們可以這樣閱讀這段型別聲明:"相等函數(shù)取兩個(gè)相同型別的值作為參數(shù)并回傳一個(gè)布林值,而這兩個(gè)參數(shù)的型別同在 Eq 類之中(即型別約束)"
Eq 這一 Typeclass 提供了判斷相等性的介面,凡是可比較相等性的型別必屬于 Eq class。
ghci> 5 == 5
True
ghci> 5 /= 5
False
ghci> 'a' == 'a'
True
ghci> "Ho Ho" == "Ho Ho"
True
ghci> 3.432 == 3.432
True
elem 函數(shù)的型別為: (Eq a)=>a->[a]->Bool。這是它在檢測(cè)值是否存在于一個(gè) List 時(shí)使用到了==的緣故。
幾個(gè)基本的 Typeclass:
Eq 包含可判斷相等性的型別。提供實(shí)現(xiàn)的函數(shù)是 == 和 /=。所以,只要一個(gè)函數(shù)有Eq類的型別限制,那么它就必定在定義中用到了 == 和 /=。剛才說(shuō)了,除函數(shù)以外的所有型別都屬于 Eq,所以它們都可以判斷相等性。
Ord 包含可比較大小的型別。除了函數(shù)以外,我們目前所談到的所有型別都屬于 Ord 類。Ord 包中包含了<, >, <=, >= 之類用于比較大小的函數(shù)。compare 函數(shù)取兩個(gè) Ord 類中的相同型別的值作參數(shù),回傳比較的結(jié)果。這個(gè)結(jié)果是如下三種型別之一:GT, LT, EQ。
ghci> :t (>)
(>) :: (Ord a) => a -> a -> Bool
型別若要成為Ord的成員,必先加入Eq家族。
ghci> "Abrakadabra" < "Zebra"
True
ghci> "Abrakadabra" `compare` "Zebra"
LT
ghci> 5 >= 2
True
ghci> 5 `compare` 3
GT
Show 的成員為可用字串表示的型別。目前為止,除函數(shù)以外的所有型別都是 Show 的成員。操作 Show Typeclass,最常用的函數(shù)表示 show。它可以取任一Show的成員型別并將其轉(zhuǎn)為字串。
ghci> show 3
"3"
ghci> show 5.334
"5.334"
ghci> show True
"True"
Read 是與 Show 相反的 Typeclass。read 函數(shù)可以將一個(gè)字串轉(zhuǎn)為 Read 的某成員型別。
ghci> read "True" || False
True
ghci> read "8.2" + 3.8
12.0
ghci> read "5" - 2
3
ghci> read "[1,2,3,4]" ++ [3]
[1,2,3,4,3]
一切良好,如上的所有型別都屬于這一 Typeclass。嘗試 read "4" 又會(huì)怎樣?
ghci> read "4"
< interactive >:1:0:
Ambiguous type variable `a' in the constraint:
`Read a' arising from a use of `read' at <interactive>:1:0-7
Probable fix: add a type signature that fixes these type variable(s)
ghci 跟我們說(shuō)它搞不清楚我們想要的是什么樣的回傳值。注意呼叫 read 后跟的那部分,ghci 通過(guò)它來(lái)辨認(rèn)其型別。若要一個(gè) boolean 值,他就知道必須得回傳一個(gè) Bool 型別的值。但在這里它只知道我們要的型別屬于 Read Typeclass,而不能明確到底是哪個(gè)??匆幌?read 函數(shù)的型別聲明吧:
ghci> :t read
read :: (Read a) => String -> a
看,它的回傳值屬于 ReadTypeclass,但我們?nèi)粲貌坏竭@個(gè)值,它就永遠(yuǎn)都不會(huì)得知該表達(dá)式的型別。所以我們需要在一個(gè)表達(dá)式后跟:: 的型別注釋,以明確其型別。如下:
ghci> read "5" :: Int
5
ghci> read "5" :: Float
5.0
ghci> (read "5" :: Float) * 4
20.0
ghci> read "[1,2,3,4]" :: [Int]
[1,2,3,4]
ghci> read "(3, 'a')" :: (Int, Char)
(3, 'a')
編譯器可以辨認(rèn)出大部分表達(dá)式的型別,但遇到 read "5" 的時(shí)候它就搞不清楚究竟該是 Int 還是 Float 了。只有經(jīng)過(guò)運(yùn)算,Haskell 才會(huì)明確其型別;同時(shí)由于 Haskell 是靜態(tài)的,它還必須得在 編譯前搞清楚所有值的型別。所以我們就最好提前給它打聲招呼:"嘿,這個(gè)表達(dá)式應(yīng)該是這個(gè)型別,省的你認(rèn)不出來(lái)!"
Enum 的成員都是連續(xù)的型別 -- 也就是可枚舉。Enum 類存在的主要好處就在于我們可以在 Range 中用到它的成員型別:每個(gè)值都有后繼子 (successer) 和前置子 (predecesor),分別可以通過(guò) succ 函數(shù)和 pred 函數(shù)得到。該 Typeclass 包含的型別有:(), Bool, Char, Ordering, Int, Integer, Float 和 Double。
ghci> ['a'..'e']
"abcde"
ghci> [LT .. GT]
[LT,EQ,GT]
ghci> [3 .. 5]
[3,4,5]
ghci> succ 'B'
'C'
Bounded 的成員都有一個(gè)上限和下限。
ghci> minBound :: Int
-2147483648
ghci> maxBound :: Char
'\1114111'
ghci> maxBound :: Bool
True
ghci> minBound :: Bool
False
minBound 和 maxBound 函數(shù)很有趣,它們的型別都是 (Bounded a) => a??梢哉f(shuō),它們都是多態(tài)常量。
如果其中的項(xiàng)都屬于 Bounded Typeclass,那么該 Tuple 也屬于 Bounded
ghci> maxBound :: (Bool, Int, Char)
(True,2147483647,'\1114111')
Num 是表示數(shù)字的 Typeclass,它的成員型別都具有數(shù)字的特征。檢查一個(gè)數(shù)字的型別:
ghci> :t 20
20 :: (Num t) => t
看樣子所有的數(shù)字都是多態(tài)常量,它可以作為所有 Num Typeclass中的成員型別。以上便是 Num Typeclass 中包含的所有型別,檢測(cè) * 運(yùn)算子的型別,可以發(fā)現(xiàn)它可以處理一切的數(shù)字:
ghci> :t (*)
(*) :: (Num a) => a -> a -> a
它只取兩個(gè)相同型別的參數(shù)。所以 (5 :: Int) * (6 :: Integer) 會(huì)引發(fā)一個(gè)型別錯(cuò)誤,而 5 * (6 :: Integer) 就不會(huì)有問(wèn)題。
型別只有親近 Show 和 Eq,才可以加入 Num。
Integral 同樣是表示數(shù)字的 Typeclass。Num 包含所有的數(shù)字:實(shí)數(shù)和整數(shù)。而 Integral 僅包含整數(shù),其中的成員型別有 Int 和 Integer。
Floating 僅包含浮點(diǎn)型別:Float 和 Double。
有個(gè)函數(shù)在處理數(shù)字時(shí)會(huì)非常有用,它便是 fromIntegral。其型別聲明為: fromIntegral :: (Num b, Integral a) => a -> b。從中可以看出,它取一個(gè)整數(shù)做參數(shù)并回傳一個(gè)更加通用的數(shù)字,這在同時(shí)處理整數(shù)和浮點(diǎn)時(shí)會(huì)尤為有用。舉例來(lái)說(shuō),length 函數(shù)的型別聲明為:length :: [a] -> Int,而非更通用的形式,如 length :: (Num b) => [a] -> b。這應(yīng)該是歷史原因吧,反正我覺(jué)得挺蠢。如果取了一個(gè) List 長(zhǎng)度的值再給它加 3.2 就會(huì)報(bào)錯(cuò),因?yàn)檫@是將浮點(diǎn)數(shù)和整數(shù)相加。面對(duì)這種情況,我們就用 fromIntegral (length [1,2,3,4]) + 3.2 來(lái)解決。
注意到,fromIntegral 的型別聲明中用到了多個(gè)型別約束。如你所見(jiàn),只要將多個(gè)型別約束放到括號(hào)里用逗號(hào)隔開(kāi)即可。