第三章 Haskell類型和類型類

2022-08-08 13:47 更新

相信類型

在前面我們談到Haskell是靜態(tài)類型的,在編譯時(shí)每個(gè)表達(dá)式的類型都已明確,這就提高了代碼的安全性。若代碼中讓布爾值與數(shù)字相除,就不會通過編譯。這樣的好處就是與其讓程序在運(yùn)行時(shí)崩潰,不如在編譯時(shí)捕獲可能的錯(cuò)誤。Haskell中萬物皆有類型,因此在執(zhí)行編譯之時(shí)編譯器可以大有所為。

與java和pascal不同,haskell支持類型推導(dǎo)。寫下一個(gè)數(shù)字,你就沒必要另告訴haskell說“它是個(gè)數(shù)字”,它自己能推導(dǎo)出來。這樣我們就不必在每個(gè)函數(shù)或表達(dá)式上都標(biāo)明其類型了。在前面我們只簡單涉及一下haskell的類型方面的知識,但是理解這一類型系統(tǒng)對于haskell 的學(xué)習(xí)是至關(guān)重要的。

類型是每個(gè)表達(dá)式都有的某種標(biāo)簽,它標(biāo)明了這一表達(dá)式所屬的范疇。例如,表達(dá)式True是boolean型,"hello"是個(gè)字符串,等等。

可以使用ghci來檢測表達(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

可以看出,:t命令處理一個(gè)表達(dá)式的輸出結(jié)果為表達(dá)式后跟::及其類型,::讀作“它的類型為”。凡是明確的類型,其首字母必為大寫。'a',如它的樣子,是Char類型,易知是個(gè)字符(character)。TrueBool類型,也靠譜。不過這又是啥,檢測"hello"得一個(gè)[Char]?這方括號表示一個(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è)過濾大寫字母的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會更清晰:removeNonUppercase :: String -> String。編譯器會自動檢測出它的類型,我們還是標(biāo)明了它的類型聲明。要是多個(gè)參數(shù)的函數(shù)該怎樣?如下便是一個(gè)將三個(gè)整數(shù)相加的簡單函數(shù)。

addThree :: Int -> Int -> Int -> Int   
addThree x y z = x + y + z

參數(shù)之間由->分隔,而與返回值之間并無特殊差異。返回值是最后一項(xiàng),參數(shù)就是前三項(xiàng)。稍后,我們將講解為何只用->而不是Int,Int,Int->Int之類“更好看”的方式來分隔參數(shù)。

如果你打算給你編寫的函數(shù)加上個(gè)類型聲明卻拿不準(zhǔn)它的類型是啥,只要先不寫類型聲明,把函數(shù)體寫出來,再使用:t命令測一下即可。函數(shù)也是表達(dá)式,所以:t對函數(shù)也是同樣可用的。

如下是幾個(gè)常見的類型:

Int表示整數(shù)。7可以是Int,但7.2不可以。Int是有界的,也就是說它由上限和下限。對32位的機(jī)器而言,上限一般是214748364,下限是-214748364。

Integer表示...厄...也是整數(shù),但它是無界的。這就意味著可以用它存放非常非常大的數(shù),我是說非常大。它的效率不如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è)字符由單引號括起,一組字符的List即為字符串。

Tuple的類型取決于它的長度及其中項(xiàng)的類型。注意,空Tuple同樣也是個(gè)類型,它只有一種值:()。

類型變量

你覺得head函數(shù)的類型是啥?它可以取任意類型的List的首項(xiàng),是怎么做到的呢?我們查一下!

ghci> :t head   
head :: [a] -> a

嗯! a是啥?類型嗎?想想我們在前面說過,凡是類型其首字母必大寫,所以它不會是個(gè)類型。它是個(gè)類型變量,意味著a可以是任意的類型。這一點(diǎn)與其他語言中的泛型(generic)很相似,但在haskell中要更為強(qiáng)大。它可以讓我們輕而易舉地寫出類型無關(guān)的函數(shù)。使用到類型變量的函數(shù)被稱作“多態(tài)函數(shù) ”,head函數(shù)的類型聲明里標(biāo)明了它可以取任意類型的List并返回其中的第一個(gè)元素。

在命名上,類型變量使用多個(gè)字符是合法的,不過約定俗成,通常都是使用單個(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是不同的類型變量,但它們不一定非得是不同的類型,它只是標(biāo)明了首項(xiàng)的類型與返回值的類型相同。

類型類101

類型定義行為的接口,如果一個(gè)類型屬于某類型類,那它必實(shí)現(xiàn)了該類型類所描述的行為。很多從OOP走過來的人們往往會把類型類當(dāng)成面向?qū)ο笳Z言中的類而感到疑惑,厄,它們不是一回事。易于理解起見,你可以把它看做是java中接口(interface)的類似物。

==函數(shù)的類型聲明是怎樣的?

ghci> :t (==)   
(==) :: (Eq a) => a -> a -> Bool

Note:判斷相等的==運(yùn)算符是函數(shù),+-*/之類的運(yùn)算符也是同樣。在默認(rèn)條件下,它們多為中綴函數(shù)。若要檢查它的類型,就必須得用括號括起使之作為另一個(gè)函數(shù),或者說以前綴函數(shù)的形式調(diào)用它。

有意思。在這里我們見到個(gè)新東西:=>符號。它左邊的部分叫做類型約束。我們可以這樣閱讀這段類型聲明:“相等函數(shù)取兩個(gè)相同類型的值作為參數(shù)并返回一個(gè)布爾值,而這兩個(gè)參數(shù)的類型同在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函數(shù)的類型為:(Eq a)=>a->[a]->Bool。這是它在檢測值是否存在于一個(gè)list時(shí)使用到了==的緣故。

幾個(gè)基本的類型類:

Eq包含可判斷相等性的類型。提供實(shí)現(xiàn)的函數(shù)是==和/=。所以,只要一個(gè)函數(shù)有Eq類的類型限制,那么它就必定在定義中用到了==和/=。剛才說了,除函數(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類型類,最常用的函數(shù)表示show。它可以取任一Show的成員類型并將其轉(zhuǎn)為字符串。

ghci> show 3   
"3"   
ghci> show 5.334   
"5.334"   
ghci> show True   
"True"

Read是與Show相反的類型類。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]

一切良好,如上的所有類型都屬于這一類型類。嘗試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跟我們說它搞不清楚我們想要的是什么樣的返回值。注意調(diào)用read后跟的那部分,ghci通過它來辨認(rèn)其類型。若要一個(gè)boolean值,他就 知道必須得返回一個(gè)Bool類型的值。但在這里它只知道我們要的類型屬于Read類型類,而不能明確到底是哪個(gè)。看一下read函數(shù)的類型聲明吧:

ghci> :t read   
read :: (Read a) => String -> a

看?它的返回值屬于Read類型類,但我們?nèi)粲貌坏竭@個(gè)值,它就永遠(yuǎn)都不會得知該表達(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)過運(yùn)算,haskell才會明確其類型;同時(shí)由于haskell是靜態(tài)的,它還必須得在 編譯前搞清楚所有值的類型。所以我們就最好提前給它打聲招呼:“嘿,這個(gè)表達(dá)式應(yīng)該是這個(gè)類型,省的你認(rèn)不出來!”

Enum的成員都是連續(xù)的類型--也就是可枚舉。Enum類存在的主要好處就在于我們可以在Range中用到它的成員類型:每個(gè)值都有后繼子(successer)和前置子(predecesor),分別可以通過succ函數(shù)和pred函數(shù)得到。該類型類包含的類型有:(),Bool,Char,Ordering,Int,Integer,FloatDouble。

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

minBoundmaxBound函數(shù)很有趣,它們的類型都是(Bounded a) => a??梢哉f,它們都是多態(tài)常量。

如果其中的項(xiàng)都屬于Bounded類型類,那么該Tuple也屬于Bounded

ghci> maxBound :: (Bool, Int, Char)   
(True,2147483647,'\1114111')

Num是表示數(shù)字的類型類,它的成員類型都具有數(shù)字的特征。檢查一個(gè)數(shù)字的類型:

ghci> :t 20   
20 :: (Num t) => t

看樣子所有的數(shù)字都是多態(tài)常量,它可以作為所有Num類型類中的成員類型。以上便是Num類型類中包含的所有類型,檢測*運(yùn)算符的類型,可以發(fā)現(xiàn)它可以處理一切的數(shù)字:

ghci> :t (*)   
(*) :: (Num a) => a -> a -> a

它只取兩個(gè)相同類型的參數(shù)。所以(5 :: Int) * (6 :: Integer)會引發(fā)一個(gè)類型錯(cuò)誤,而5 * (6 :: Integer)就不會有問題。

類型只有親近ShowEq,才可以加入Num

Integral同樣是表示數(shù)字的類型類。Num包含所有的數(shù)字:實(shí)數(shù)和整數(shù)。而Intgral僅包含整數(shù),其中的成員類型有Int和Integer。

Floating僅包含浮點(diǎn)類型:Float和Double。

有個(gè)函數(shù)在處理數(shù)字時(shí)會非常有用,它便是fromIntegral。其類型聲明為:fromIntegral :: (Num b, Integral a) => a -> b。從中可以看出,它取一個(gè)整數(shù)做參數(shù)并返回一個(gè)更加通用的數(shù)字,這在同時(shí)處理整數(shù)和浮點(diǎn)時(shí)會尤為有用。舉例來說,length函數(shù)的類型聲明為:length :: [a] -> Int,而非更通用的形式,如(Num b) => length :: [a] -> b。這應(yīng)該時(shí)歷史原因吧,反正我覺得挺蠢。如果取了一個(gè)List長度的值再給它加3.2就會報(bào)錯(cuò),因?yàn)檫@是將浮點(diǎn)數(shù)和整數(shù)相加。面對這種情況,我們就用fromIntegral (length [1,2,3,4]) + 3.2來解決。

注意到,fromIntegral的類型聲明中用到了多個(gè)類型約束。如你所見,只要將多個(gè)類型約束放到括號里用逗號隔開即可。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號