## 相信類型

在前面我們談到Haskell是靜態類型的,在編譯時每個表達式的類型都已明確,這就提高了代碼的安全性。若代碼中讓布爾值與數字相除,就不會通過編譯。這樣的好處就是與其讓程序在運行時崩潰,不如在編譯時捕獲可能的錯誤。Haskell中萬物皆有類型,因此在執行編譯之時編譯器可以大有所為。
與java和pascal不同,haskell支持類型推導。寫下一個數字,你就沒必要另告訴haskell說“它是個數字”,它自己能推導出來。這樣我們就不必在每個函數或表達式上都標明其類型了。在前面我們只簡單涉及一下haskell的類型方面的知識,但是理解這一類型系統對于haskell 的學習是至關重要的。
類型是每個表達式都有的某種標簽,它標明了這一表達式所屬的范疇。例如,表達式True是boolean型,"hello"是個字符串,等等。
可以使用ghci來檢測表達式的類型。使用:t命令后跟任何可用的表達式,即可得到該表達式的類型,先試一下:
~~~
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
~~~

可以看出,`:t`命令處理一個表達式的輸出結果為表達式后跟`::`及其類型,`::`讀作“它的類型為”。凡是明確的類型,其首字母必為大寫。`'a'`,如它的樣子,是`Char`類型,易知是個字符(character)。`True`是`Bool`類型,也靠譜。不過這又是啥,檢測`"hello"`得一個`[Char]`?這方括號表示一個List,所以我們可以將其讀作“一組字符的List”。而與List不同,每個Tuple都是獨立的類型,于是`(True,"a")`的類型是`(Bool,Char)`,而`('a','b','c')`的類型為`(Char,Char,Char)`。`4==5`一定返回 False,所以它的類型為Bool。
同樣,函數也有類型。編寫函數時,給它一個明確的類型聲明是個好習慣,比較短的函數就不用多此一舉了。還記得前面那個過濾大寫字母的List Comprehension嗎?給它加上類型聲明便是這個樣子:
~~~
removeNonUppercase?::?[Char]?->?[Char]???
removeNonUppercase?st?=?[?c?|?c??st,?c?`elem`?['A'..'Z']]
~~~
`removeNonUppercase`的類型為`[Char]->[Char]`,從它的參數和返回值的類型上可以看出,它將一個字符串映射為另一個字符串。`[Char]`與`String`是等價的,但使用String會更清晰:`removeNonUppercase :: String -> String`。編譯器會自動檢測出它的類型,我們還是標明了它的類型聲明。要是多個參數的函數該怎樣?如下便是一個將三個整數相加的簡單函數。
~~~
addThree?::?Int?->?Int?->?Int?->?Int???
addThree?x?y?z?=?x?+?y?+?z
~~~
參數之間由->分隔,而與返回值之間并無特殊差異。返回值是最后一項,參數就是前三項。稍后,我們將講解為何只用->而不是`Int,Int,Int->Int`之類“更好看”的方式來分隔參數。
如果你打算給你編寫的函數加上個類型聲明卻拿不準它的類型是啥,只要先不寫類型聲明,把函數體寫出來,再使用:t命令測一下即可。函數也是表達式,所以:t對函數也是同樣可用的。
如下是幾個常見的類型:
**Int**表示整數。7可以是Int,但7.2不可以。Int是有界的,也就是說它由上限和下限。對32位的機器而言,上限一般是214748364,下限是-214748364。
**Integer**表示...厄...也是整數,但它是無界的。這就意味著可以用它存放非常非常大的數,我是說非常大。它的效率不如Int高。
~~~
factorial?::?Integer?->?Integer???
factorial?n?=?product?[1..n]
~~~
~~~
ghci>?factorial?50???
30414093201713378043612608166064768844377641568960512000000000000
~~~
**Float**表示單精度的浮點數。
~~~
circumference?::?Float?->?Float???
circumference?r?=?2?*?pi?*?r
~~~
ghci>?circumference?4.0???
25.132742
**Double**表示雙精度的浮點數。
~~~
circumference'?::?Double?->?Double???
circumference'?r?=?2?*?pi?*?r
ghci>?circumference'?4.0???
25.132741228718345
~~~
**Bool**表示布爾值,它只有兩種值:True和False。
**Char**表示一個字符。一個字符由單引號括起,一組字符的List即為字符串。
Tuple的類型取決于它的長度及其中項的類型。注意,空Tuple同樣也是個類型,它只有一種值:`()`。
## 類型變量
你覺得head函數的類型是啥?它可以取任意類型的List的首項,是怎么做到的呢?我們查一下!
~~~
ghci>?:t?head???
head?::?[a]?->?a
~~~

嗯! a是啥?類型嗎?想想我們在前面說過,凡是類型其首字母必大寫,所以它不會是個類型。它是個類型變量,意味著a可以是任意的類型。這一點與其他語言中的泛型(generic)很相似,但在haskell中要更為強大。它可以讓我們輕而易舉地寫出類型無關的函數。使用到類型變量的函數被稱作“多態函數 ”,head函數的類型聲明里標明了它可以取任意類型的List并返回其中的第一個元素。
在命名上,類型變量使用多個字符是合法的,不過約定俗成,通常都是使用單個字符,如a,b,c,d...
還記得fst?我們查一下它的類型:
~~~
ghci>?:t?fst???
fst?::?(a,?b)?->?a
~~~
可以看到fst取一個包含兩個類型的Tuple作參數,并以第一個項的類型作為返回值。這便是fst可以處理一個含有兩種類型項的pair的原因。注意,a和b是不同的類型變量,但它們不一定非得是不同的類型,它只是標明了首項的類型與返回值的類型相同。
## 類型類101

類型定義行為的接口,如果一個類型屬于某類型類,那它必實現了該類型類所描述的行為。很多從OOP走過來的人們往往會把類型類當成面向對象語言中的類而感到疑惑,厄,它們不是一回事。易于理解起見,你可以把它看做是java中接口(interface)的類似物。
==函數的類型聲明是怎樣的?
~~~
ghci>?:t?(==)???
(==)?::?(Eq?a)?=>?a?->?a?->?Bool
~~~
> **Note**:判斷相等的==運算符是函數,+-*/之類的運算符也是同樣。在默認條件下,它們多為中綴函數。若要檢查它的類型,就必須得用括號括起使之作為另一個函數,或者說以前綴函數的形式調用它。
有意思。在這里我們見到個新東西:=>符號。它左邊的部分叫做類型約束。我們可以這樣閱讀這段類型聲明:“相等函數取兩個相同類型的值作為參數并返回一個布爾值,而這兩個參數的類型同在Eq類之中(即類型約束)”
**Eq**這一類型類提供了判斷相等性的接口,凡是可比較相等性的類型必屬于Eq類。
~~~
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函數的類型為:`(Eq a)=>a->[a]->Bool`。這是它在檢測值是否存在于一個list時使用到了==的緣故。
幾個基本的類型類:
**Eq**包含可判斷相等性的類型。提供實現的函數是==和/=。所以,只要一個函數有Eq類的類型限制,那么它就必定在定義中用到了==和/=。剛才說了,除函數意外的所有類型都屬于Eq,所以它們都可以判斷相等性。
**Ord**包含可比較大小的類型。除了函數以外,我們目前所談到的所有類型都屬于Ord類。Ord包中包含了,=之類用于比較大小的函數。compare函數取兩個Ord類中的相同類型的值作參數,返回比較的結果。這個結果是如下三種類型之一: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**的成員為可用字符串表示的類型。目前為止,除函數以外的所有類型都是Show的成員。操作Show類型類,最常用的函數表示show。它可以取任一Show的成員類型并將其轉為字符串。
~~~
ghci>?show?3???
"3"???
ghci>?show?5.334???
"5.334"???
ghci>?show?True???
"True"
~~~
**Read**是與Show相反的類型類。read函數可以將一個字符串轉為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]
~~~
一切良好,如上的所有類型都屬于這一類型類。嘗試read "4"又會怎樣?
~~~
ghci> read "4"
< interactive >:1:0:
Ambiguous type variable `a' in the constraint:
`Read a' arising from a use of `read' at :1:0-7
Probable fix: add a type signature that fixes these type variable(s)
~~~
ghci跟我們說它搞不清楚我們想要的是什么樣的返回值。注意調用read后跟的那部分,ghci通過它來辨認其類型。若要一個boolean值,他就 知道必須得返回一個Bool類型的值。但在這里它只知道我們要的類型屬于Read類型類,而不能明確到底是哪個。看一下read函數的類型聲明吧:
~~~
ghci>?:t?read???
read?::?(Read?a)?=>?String?->?a
~~~
看?它的返回值屬于Read類型類,但我們若用不到這個值,它就永遠都不會得知該表達式的類型。所以我們需要在一個表達式后跟`::`的**類型注釋**,以明確其類型。如下:
~~~
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')
~~~
編譯器可以辨認出大部分表達式的類型,但遇到`read "5"`的時候它就搞不清楚究竟該是Int還是Float了。只有經過運算,haskell才會明確其類型;同時由于haskell是靜態的,它還必須得在 編譯前搞清楚所有值的類型。所以我們就最好提前給它打聲招呼:“嘿,這個表達式應該是這個類型,省的你認不出來!”
**Enum**的成員都是連續的類型--也就是可枚舉。Enum類存在的主要好處就在于我們可以在Range中用到它的成員類型:每個值都有后繼子(successer)和前置子(predecesor),分別可以通過succ函數和pred函數得到。該類型類包含的類型有:`()`,`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**的成員都有一個上限和下限。
~~~
ghci>?minBound?::?Int???
-2147483648???
ghci>?maxBound?::?Char???
'\1114111'???
ghci>?maxBound?::?Bool???
True???
ghci>?minBound?::?Bool???
False
~~~
`minBound`和`maxBound`函數很有趣,它們的類型都是`(Bounded a) => a`。可以說,它們都是多態常量。
如果其中的項都屬于`Bounded`類型類,那么該Tuple也屬于`Bounded`
~~~
ghci>?maxBound?::?(Bool,?Int,?Char)???
(True,2147483647,'\1114111')
~~~
**Num**是表示數字的類型類,它的成員類型都具有數字的特征。檢查一個數字的類型:
~~~
ghci>?:t?20???
20?::?(Num?t)?=>?t
~~~
看樣子所有的數字都是多態常量,它可以作為所有`Num`類型類中的成員類型。以上便是`Num`類型類中包含的所有類型,檢測*運算符的類型,可以發現它可以處理一切的數字:
~~~
ghci>?:t?(*)???
(*)?::?(Num?a)?=>?a?->?a?->?a
~~~
它只取兩個相同類型的參數。所以`(5 :: Int) * (6 :: Integer)`會引發一個類型錯誤,而`5 * (6 :: Integer)`就不會有問題。
類型只有親近`Show`和`Eq`,才可以加入`Num`。
**Integral**同樣是表示數字的類型類。Num包含所有的數字:實數和整數。而Intgral僅包含整數,其中的成員類型有Int和Integer。
**Floating**僅包含浮點類型:Float和Double。
有個函數在處理數字時會非常有用,它便是`fromIntegral`。其類型聲明為:`fromIntegral :: (Num b, Integral a) => a -> b`。從中可以看出,它取一個整數做參數并返回一個更加通用的數字,這在同時處理整數和浮點時會尤為有用。舉例來說,`length`函數的類型聲明為:`length :: [a] -> Int`,而非更通用的形式,如`(Num b) => length :: [a] -> b`。這應該時歷史原因吧,反正我覺得挺蠢。如果取了一個List長度的值再給它加3.2就會報錯,因為這是將浮點數和整數相加。面對這種情況,我們就用`fromIntegral (length [1,2,3,4]) + 3.2`來解決。
注意到,`fromIntegral`的類型聲明中用到了多個類型約束。如你所見,只要將多個類型約束放到括號里用逗號隔開即可。