類型類(typeclass)是 Haskell 最強(qiáng)大的功能之一:它用于定義通用接口,為各種不同的類型提供一組公共特性集。
類型類是某些基本語(yǔ)言特性的核心,比如相等性測(cè)試和數(shù)值操作符。
在討論如何使用類型類之前,先來看看它能做什么。
假設(shè)這樣一個(gè)場(chǎng)景:我們想對(duì) Color 類型的值進(jìn)行對(duì)比,但 Haskell 的語(yǔ)言設(shè)計(jì)者卻沒有實(shí)現(xiàn) == 操作。
要解決這個(gè)問題,必須親自實(shí)現(xiàn)一個(gè)相等性測(cè)試函數(shù):
-- file: ch06/colorEq.hs
data Color = Red | Green | Blue
colorEq :: Color -> Color -> Bool
colorEq Red Red = True
colorEq Green Green = True
colorEq Blue Blue = True
colorEq _ _ = False
在 ghci 里測(cè)試:
Prelude> :load colorEq.hs
[1 of 1] Compiling Main ( colorEq.hs, interpreted )
Ok, modules loaded: Main.
*Main> colorEq Green Green
True
*Main> colorEq Blue Red
False
過了一會(huì),程序又添加了一個(gè)新類型 —— 職位:它對(duì)公司中的各個(gè)員工進(jìn)行分類。
在執(zhí)行像是工資計(jì)算這類任務(wù)是,又需要用到相等性測(cè)試,所以又需要再次為職位類型定義相等性測(cè)試函數(shù):
-- file: ch06/roleEq.hs
data Role = Boss | Manager | Employee
roleEq :: Role -> Role -> Bool
roleEq Employee Employee = True
roleEq Manager Manager = True
roleEq Boss Boss = True
roleEq _ _ = False
測(cè)試:
Prelude> :load roleEq.hs
[1 of 1] Compiling Main ( roleEq.hs, interpreted )
Ok, modules loaded: Main.
*Main> roleEq Boss Boss
True
*Main> roleEq Boss Employee
False
colorEq 和 roleEq 的定義揭示了一個(gè)問題:對(duì)于每個(gè)不同的類型,我們都需要為它們專門定義一個(gè)對(duì)比函數(shù)。
這種做法非常低效,而且煩人。如果同一個(gè)對(duì)比函數(shù)(比如 == )可以用于對(duì)比任何類型的值,這樣就會(huì)方便得多。
另一方面,一般來說,如果定義了相等測(cè)試函數(shù)(比如 == ),那么不等測(cè)試函數(shù)(比如 /= )的值就可以直接對(duì)相等測(cè)試函數(shù)取反(使用 not )來計(jì)算得出。因此,如果可以通過相等測(cè)試函數(shù)來定義不等測(cè)試函數(shù),那么會(huì)更方便。
通用函數(shù)還可以讓代碼變得更通用:如果同一段代碼可以用于不同類型的輸入值,那么程序的代碼量將大大減少。
還有很重要的一點(diǎn)是,如果在之后添加通用函數(shù)對(duì)新類型的支持,那么原來的代碼應(yīng)該不需要進(jìn)行修改。
Haskell 的類型類可以滿足以上提到的所有要求。
類型類定義了一系列函數(shù),這些函數(shù)對(duì)于不同類型的值使用不同的函數(shù)實(shí)現(xiàn)。它和其他語(yǔ)言的接口和多態(tài)方法有些類似。
[譯注:這里原文是將“面向?qū)ο缶幊讨械膶?duì)象”和 Haskell 的類型類進(jìn)行類比,但實(shí)際上這種類比并不太恰當(dāng),類比成接口和多態(tài)方法更適合一點(diǎn)。]
我們定義一個(gè)類型類來解決前面提到的相等性測(cè)試問題:
class BasicEq a where
isEqual :: a -> a -> Bool
類型類使用 class 關(guān)鍵字來定義,跟在 class 之后的 BasicEq 是這個(gè)類型類的名字,之后的 a 是這個(gè)類型類的實(shí)例類型(instance type)。
BasicEq 使用類型變量 a 來表示實(shí)例類型,說明它并不將這個(gè)類型類限定于某個(gè)類型:任何一個(gè)類型,只要它實(shí)現(xiàn)了這個(gè)類型類中定義的函數(shù),那么它就是這個(gè)類型類的實(shí)例類型。
實(shí)例類型所使用的名字可以隨意選擇,但是它和類型類中定義函數(shù)簽名時(shí)所使用的名字應(yīng)該保持一致。比如說,我們使用 a 來表示實(shí)例類型,那么函數(shù)簽名中也必須使用 a 來代表這個(gè)實(shí)例類型。
BasicEq 類型類只定義了 isEqual 一個(gè)函數(shù) —— 它接受兩個(gè)參數(shù)作為輸入,并且這兩個(gè)參數(shù)都指向同一種實(shí)例類型:
Prelude> :load BasicEq_1.hs
[1 of 1] Compiling Main ( BasicEq_1.hs, interpreted )
Ok, modules loaded: Main.
*Main> :type isEqual
isEqual :: BasicEq a => a -> a -> Bool
作為演示,以下代碼將 Bool 類型作為 BasicEq 的實(shí)例類型,實(shí)現(xiàn)了 isEqual 函數(shù):
instance BasicEq Bool where
isEqual True True = True
isEqual False False = True
isEqual _ _ = False
在 ghci 里驗(yàn)證這個(gè)程序:
*Main> isEqual True True
True
*Main> isEqual False True
False
如果試圖將不是 BasicEq 實(shí)例類型的值作為輸入調(diào)用 isEqual 函數(shù),那么就會(huì)引發(fā)錯(cuò)誤:
*Main> isEqual "hello" "moto"
<interactive>:5:1:
No instance for (BasicEq [Char])
arising from a use of `isEqual'
Possible fix: add an instance declaration for (BasicEq [Char])
In the expression: isEqual "hello" "moto"
In an equation for `it': it = isEqual "hello" "moto"
錯(cuò)誤信息提醒我們, [Char] 并不是 BasicEq 的實(shí)例類型。
稍后的一節(jié)會(huì)介紹更多關(guān)于類型類實(shí)例的定義方式,這里先繼續(xù)前面的例子。這一次,除了 isEqual 之外,我們還想定義不等測(cè)試函數(shù) isNotEqual :
class BasicEq a where
isEqual :: a -> a -> Bool
isNotEqual :: a -> a -> Bool
同時(shí)定義 isEqual 和 isNotEqual 兩個(gè)函數(shù)產(chǎn)生了一些不必要的工作:從邏輯上講,對(duì)于任何類型,只要知道 isEqual 或 isNotEqual 的任意一個(gè),就可以計(jì)算出另外一個(gè)。因此,一種更省事的辦法是,為 isEqual 和 isNotEqual 兩個(gè)函數(shù)提供默認(rèn)值,這樣 BasicEq 的實(shí)例類型只要實(shí)現(xiàn)這兩個(gè)函數(shù)中的一個(gè),就可以順利使用這兩個(gè)函數(shù):
class BasicEq a where
isEqual :: a -> a -> Bool
isEqual x y = not (isNotEqual x y)
isNotEqual :: a -> a -> Bool
isNotEqual x y = not (isEqual x y)
以下是將 Bool 作為 BasicEq 實(shí)例類型的例子:
instance BasicEq Bool where
isEqual False False = True
isEqual True True = True
isEqual _ _ = False
我們只要定義 isEqual 函數(shù),就可以“免費(fèi)”得到 isNotEqual :
Prelude> :load BasicEq_3.hs
[1 of 1] Compiling Main ( BasicEq_3.hs, interpreted )
Ok, modules loaded: Main.
*Main> isEqual True True
True
*Main> isEqual False False
True
*Main> isNotEqual False True
True
當(dāng)然,如果閑著沒事,你仍然可以自己親手定義這兩個(gè)函數(shù)。但是,你至少要定義兩個(gè)函數(shù)中的一個(gè),否則兩個(gè)默認(rèn)的函數(shù)就會(huì)互相調(diào)用,直到程序崩潰。
定義一個(gè)類型為某個(gè)類型類的實(shí)例,指的就是,為某個(gè)類型實(shí)現(xiàn)給定類型類所聲明的全部函數(shù)。
比如在前面, BasicEq 類型類定義了兩個(gè)函數(shù) isEqual 和 isNotEqual :
class BasicEq a where
isEqual :: a -> a -> Bool
isEqual x y = not (isNotEqual x y)
isNotEqual :: a -> a -> Bool
isNotEqual x y = not (isEqual x y)
在前一節(jié),我們成功將 Bool 類型實(shí)現(xiàn)為 BasicEq 的實(shí)例類型,要使 Color 類型也成為 BasicEq 類型類的實(shí)例,就需要另外為 Color 類型實(shí)現(xiàn) isEqual 和 isNotEqual :
instance BasicEq Color where
isEqual Red Red = True
isEqual Blue Blue = True
isEqual Green Green = True
isEqual _ _ = True
注意,這里的函數(shù)定義和之前的 colorEq 函數(shù)定義實(shí)際上沒有什么不同,唯一的區(qū)別是,它使得 isEqual 不僅可以對(duì) Bool 類型進(jìn)行對(duì)比測(cè)試,還可以對(duì) Color 類型進(jìn)行對(duì)比測(cè)試。
更一般地說,只要為相應(yīng)的類型實(shí)現(xiàn) BasicEq 類型類中的定義,那么 isEqual 就可以用于對(duì)比任何我們想對(duì)比的類型。
不過在實(shí)際中,通常并不使用 BasicEq 類型類,而是使用 Haskell Report 中定義的 Eq 類型類:它定義了 == 和 /= 操作符,這兩個(gè)操作符才是 Haskell 中最常用的測(cè)試函數(shù)。
以下是 Eq 類型類的定義:
class Eq a where
(==), (/=) :: a -> a -> Bool
-- Minimal complete definition:
-- (==) or (/=)
x /= y = not (x == y)
x == y = not (x /= y)
稍后會(huì)介紹更多使用 Eq 類型類的信息。
前面兩節(jié)分別介紹了類型類的定義,以及如何讓某個(gè)類型成為給定類型類的實(shí)例類型。
正本節(jié)會(huì)介紹幾個(gè) Prelude 庫(kù)中包含的類型類。如本章開始時(shí)所說的,類型類是 Haskell 語(yǔ)言某些特性的奠基石,本節(jié)就會(huì)介紹幾個(gè)這方面的例子。
更多信息可以參考 Haskell 的函數(shù)參考,那里一般都給出了類型類的詳細(xì)介紹,并且說明,要成為這個(gè)類型類的實(shí)例,需要實(shí)現(xiàn)那些函數(shù)。
Show 類型類用于將值轉(zhuǎn)換為字符串,它最重要的函數(shù)是 show 。
show 函數(shù)使用單個(gè)參數(shù)接收輸入數(shù)據(jù),并返回一個(gè)表示該輸入數(shù)據(jù)的字符串:
Main> :type show
show :: Show a => a -> String
以下是一些 show 函數(shù)調(diào)用的例子:
Main> show 1
"1"
Main> show [1, 2, 3]
"[1,2,3]"
Main> show (1, 2)
"(1,2)"
Ghci 輸出一個(gè)值,實(shí)際上就是對(duì)這個(gè)值調(diào)用 putStrLn 和 show :
Main> 1
1
Main> show 1
"1"
Main> putStrLn (show 1)
1
因此,如果你定義了一種新的數(shù)據(jù)類型,并且希望通過 ghci 來顯示它,那么你就應(yīng)該將這個(gè)類型實(shí)現(xiàn)為 Show 類型類的實(shí)例,否則 ghci 就會(huì)向你抱怨,說它不知道該怎樣用字符串的形式表示這種數(shù)據(jù)類型:
Main> data Color = Red | Green | Blue;
Main> show Red
<interactive>:10:1:
No instance for (Show Color)
arising from a use of `show'
Possible fix: add an instance declaration for (Show Color)
In the expression: show Red
In an equation for `it': it = show Red
Prelude> Red
<interactive>:5:1:
No instance for (Show Color)
arising from a use of `print'
Possible fix: add an instance declaration for (Show Color)
In a stmt of an interactive GHCi command: print it
通過實(shí)現(xiàn) Color 類型的 show 函數(shù),讓 Color 類型成為 Show 的類型實(shí)例,可以解決以上問題:
instance Show Color where
show Red = "Red"
show Green = "Green"
show Blue = "Blue"
當(dāng)然, show 函數(shù)的打印值并不是非要和類型構(gòu)造器一樣不可,比如 Red 值并不是非要表示為 "Red" 不可,以下是另一種實(shí)例化 Show 類型類的方式:
instance Show Color where
show Red = "Color 1: Red"
show Green = "Color 2: Green"
show Blue = "Color 3: Blue"
Read 和 Show 類型類的作用正好相反,它將字符串轉(zhuǎn)換為值。
Read 最有用的函數(shù)是 read :它接受一個(gè)字符串作為參數(shù),對(duì)這個(gè)字符串進(jìn)行處理,并返回一個(gè)值,這個(gè)值的類型為 Read 實(shí)例類型的成員(所有實(shí)例類型中的一種)。
Prelude> :type read
read :: Read a => String -> a
以下代碼展示了 read 的用法:
Prelude> read "3"
<interactive>:5:1:
Ambiguous type variable `a0' in the constraint:
(Read a0) arising from a use of `read'
Probable fix: add a type signature that fixes these type variable(s)
In the expression: read "3"
In an equation for `it': it = read "3"
Prelude> (read "3")::Int
3
Prelude> :type it
it :: Int
Prelude> (read "3")::Double
3.0
Prelude> :type it
it :: Double
注意在第一次調(diào)用 read 的時(shí)候,我們并沒有顯式地給定類型簽名,這時(shí)對(duì) read"3" 的求值會(huì)引發(fā)錯(cuò)誤。這是因?yàn)橛蟹浅6嗟念愋投际?Read 的實(shí)例,而編譯器在 read 函數(shù)讀入 "3" 之后,不知道應(yīng)該將這個(gè)值轉(zhuǎn)換成什么類型,于是編譯器就會(huì)向我們發(fā)牢騷。
因此,為了讓 read 函數(shù)返回正確類型的值,必須給它指示正確的類型。
很多時(shí)候,程序需要將內(nèi)存中的數(shù)據(jù)保存為文件,又或者,反過來,需要將文件中的數(shù)據(jù)轉(zhuǎn)換為內(nèi)存中的數(shù)據(jù)實(shí)體。這種轉(zhuǎn)換過程稱為序列化和反序列化 .
通過將類型實(shí)現(xiàn)為 Read 和 Show 的實(shí)例類型, read 和 show 兩個(gè)函數(shù)可以成為非常好的序列化工具。
作為例子,以下代碼將一個(gè)內(nèi)存中的列表序列化到文件中:
Prelude> let years = [1999, 2010, 2012]
Prelude> show years
"[1999,2010,2012]"
Prelude> writeFile "years.txt" (show years)
writeFile 將給定內(nèi)容寫入到文件當(dāng)中,它接受兩個(gè)參數(shù),第一個(gè)參數(shù)是文件路徑,第二個(gè)參數(shù)是寫入到文件的字符串內(nèi)容。
觀察文件 years.txt 可以看到, (showyears) 所產(chǎn)生的文本被成功保存到了文件當(dāng)中:
$ cat years.txt
[1999,2010,2012]
使用以下代碼可以對(duì) years.txt 進(jìn)行反序列化操作:
Prelude> input <- readFile "years.txt"
Prelude> input -- 讀入的字符串
"[1999,2010,2012]"
Prelude> (read input)::[Int] -- 將字符串轉(zhuǎn)換成列表
[1999,2010,2012]
readFile 讀入給定的 years.txt ,并將它的內(nèi)存?zhèn)鹘o input 變量,最后,通過使用 read ,我們成功將字符串反序列化成一個(gè)列表。
Haskell 有一集非常強(qiáng)大的數(shù)字類型:從速度飛快的 32 位或 64 位整數(shù),到任意精度的有理數(shù),包羅萬有。
除此之外,Haskell 還有一系列通用算術(shù)操作符,這些操作符可以用于幾乎所有數(shù)字類型。而對(duì)數(shù)字類型的這種強(qiáng)有力的支持就是建立在類型類的基礎(chǔ)上的。
作為一個(gè)額外的好處(side benefit),用戶可以定義自己的數(shù)字類型,并且獲得和內(nèi)置數(shù)字類型完全平等的權(quán)利。
以下表格顯示了 Haskell 中最常用的一些數(shù)字類型:
表格 6.1 : 部分?jǐn)?shù)字類型
類型 | 介紹 |
---|---|
Double | 雙精度浮點(diǎn)數(shù)。表示浮點(diǎn)數(shù)的常見選擇。 |
Float | 單精度浮點(diǎn)數(shù)。通常在對(duì)接 C 程序時(shí)使用。 |
Int | 固定精度帶符號(hào)整數(shù);最小范圍在 -2^29 至 2^29-1 。相當(dāng)常用。 |
Int8 | 8 位帶符號(hào)整數(shù) |
Int16 | 16 位帶符號(hào)整數(shù) |
Int32 | 32 位帶符號(hào)整數(shù) |
Int64 | 64 位帶符號(hào)整數(shù) |
Integer | 任意精度帶符號(hào)整數(shù);范圍由機(jī)器的內(nèi)存限制。相當(dāng)常用。 |
Rational | 任意精度有理數(shù)。保存為兩個(gè)整數(shù)之比(ratio)。 |
Word | 固定精度無符號(hào)整數(shù)。占用的內(nèi)存大小和 Int 相同 |
Word8 | 8 位無符號(hào)整數(shù) |
Word16 | 16 位無符號(hào)整數(shù) |
Word32 | 32 位無符號(hào)整數(shù) |
Word64 | 64 位無符號(hào)整數(shù) |
大部分算術(shù)操作都可以用于任意數(shù)字類型,少數(shù)的一部分函數(shù),比如 asin ,只能用于浮點(diǎn)數(shù)類型。
以下表格列舉了操作各種數(shù)字類型的常見函數(shù)和操作符:
表格 6.2 : 部分?jǐn)?shù)字函數(shù)和
項(xiàng) | 類型 | 模塊 | 描述 | |
---|---|---|---|---|
(+) | Num a => a -> a -> a | Prelude | 加法 | |
(-) | Num a => a -> a -> a | Prelude | 減法 | |
(*) | Num a => a -> a -> a | Prelude | 乘法 | |
(/) | Fractional a => a -> a -> a | Prelude | 份數(shù)除法 | |
(**) | Floating a => a -> a -> a | Prelude | 乘冪 | |
(^) | (Num a, Integral b) => a -> b -> a | Prelude | 計(jì)算某個(gè)數(shù)的非負(fù)整數(shù)次方 | |
(^^) | (Fractional a, Integral b) => a -> b -> a | Prelude | 分?jǐn)?shù)的任意整數(shù)次方 | |
(%) | Integral a => a -> a -> Ratio a | Data.Ratio | 構(gòu)成比率 | |
(.&.) | Bits a => a -> a -> a | Data.Bits | 二進(jìn)制并操作 | |
(. | .) | Bits a => a -> a -> a | Data.Bits | 二進(jìn)制或操作 |
abs | Num a => a -> a | Prelude | 絕對(duì)值操作 | |
approxRational | RealFrac a => a -> a -> Rational | Data.Ratio | 通過分?jǐn)?shù)的分子和分母計(jì)算出近似有理數(shù) | |
cos | Floating a => a -> a | Prelude | 余弦函數(shù)。另外還有 acos 、 cosh 和 acosh ,類型和 cos 一樣。 | |
div | Integral a => a -> a -> a | Prelude | 整數(shù)除法,總是截?cái)嘈?shù)位。 | |
fromInteger | Num a => Integer -> a | Prelude | 將一個(gè) Integer 值轉(zhuǎn)換為任意數(shù)字類型。 | |
fromIntegral | (Integral a, Num b) => a -> b | Prelude | 一個(gè)更通用的轉(zhuǎn)換函數(shù),將任意 Integral 值轉(zhuǎn)為任意數(shù)字類型。 | |
fromRational | Fractional a => Rational -> a | Prelude | 將一個(gè)有理數(shù)轉(zhuǎn)換為分?jǐn)?shù)??赡軙?huì)有精度損失。 | |
log | Floating a => a -> a | Prelude | 自然對(duì)數(shù)算法。 | |
logBase | Floating a => a -> a -> a | Prelude | 計(jì)算指定底數(shù)對(duì)數(shù)。 | |
maxBound | Bounded a => a | Prelude | 有限長(zhǎng)度數(shù)字類型的最大值。 | |
minBound | Bounded a => a | Prelude | 有限長(zhǎng)度數(shù)字類型的最小值。 | |
mod | Integral a => a -> a -> a | Prelude | 整數(shù)取模。 | |
pi | Floating a => a | Prelude | 圓周率常量。 | |
quot | Integral a => a -> a -> a | Prelude | 整數(shù)除法;商數(shù)的分?jǐn)?shù)部分截?cái)酁?0 。 | |
recip | Fractional a => a -> a | Prelude | 分?jǐn)?shù)的倒數(shù)。 | |
rem | Integral a => a -> a -> a | Prelude | 整數(shù)除法的余數(shù)。 | |
round | (RealFrac a, Integral b) => a -> b | Prelude | 四舍五入到最近的整數(shù)。 | |
shift | Bits a => a -> Int -> a | Bits | 輸入為正整數(shù),就進(jìn)行左移。如果為負(fù)數(shù),進(jìn)行右移。 | |
sin | Floating a => a -> a | Prelude | 正弦函數(shù)。還提供了 asin 、 sinh 和 asinh ,和 sin 類型一樣。 | |
sqrt | Floating a => a -> a | Prelude | 平方根 | |
tan | Floating a => a -> a | Prelude | 正切函數(shù)。還提供了 atan 、 tanh 和 atanh ,和 tan 類型一樣。 | |
toInteger | Integral a => a -> Integer | Prelude | 將任意 Integral 值轉(zhuǎn)換為 Integer | |
toRational | Real a => a -> Rational | Prelude | 從實(shí)數(shù)到有理數(shù)的有損轉(zhuǎn)換 | |
truncate | (RealFrac a, Integral b) => a -> b | Prelude | 向下取整 | |
xor | Bits a => a -> a -> a | Data.Bits | 二進(jìn)制異或操作 |
數(shù)字類型及其對(duì)應(yīng)的類型類列舉在下表:
表格 6.3 : 數(shù)字類型的類型類實(shí)例
類型 | Bits | Bounded | Floating | Fractional | Integral | Num | Real | RealFrac |
---|---|---|---|---|---|---|---|---|
Double | ? | ? | X | X | ? | X | X | X |
Float | ? | ? | X | X | ? | X | X | X |
Int | X | X | ? | ? | X | X | X | ? |
Int16 | X | X | ? | ? | X | X | X | ? |
Int32 | X | X | ? | ? | X | X | X | ? |
Int64 | X | X | ? | ? | X | X | X | ? |
Integer | X | ? | ? | ? | X | X | X | ? |
Rational or any Ratio | ? | ? | ? | X | ? | X | X | X |
Word | X | X | ? | ? | X | X | X | ? |
Word16 | X | X | ? | ? | X | X | X | ? |
Word32 | X | X | ? | ? | X | X | X | ? |
Word64 | X | X | ? | ? | X | X | X | ? |
表格 6.2 列舉了一些數(shù)字類型之間進(jìn)行轉(zhuǎn)換的函數(shù),以下表格是一個(gè)匯總:
表格 6.4 : 數(shù)字類型之間的轉(zhuǎn)換
源類型 | 目標(biāo)類型 | |||
Double, Float | Int, Word | Integer | Rational | |
Double, FloatInt, WordIntegerRational | fromRational . toRationalfromIntegralfromIntegralfromRational | truncate *fromIntegralfromIntegraltruncate * | truncate *fromIntegralN/Atruncate * | toRationalfromIntegralfromIntegralN/A |
第十三章會(huì)說明,怎樣用自定義數(shù)據(jù)類型來擴(kuò)展數(shù)字類型。
除了前面介紹的通用算術(shù)符號(hào)之外,相等測(cè)試、不等測(cè)試、大于和小于等對(duì)比操作也是非常常見的。
其中, Eq 類型類定義了 == 和 /= 操作,而 >= 和 <= 等對(duì)比操作,則由 Ord 類型類定義。
需要將對(duì)比操作和相等性測(cè)試分開用兩個(gè)類型類來定義的原因是,對(duì)于某些類型,它們只對(duì)相等性測(cè)試和不等測(cè)試有興趣,比如 Handle 類型,而部分有序操作(particular ordering, 大于、小于等)對(duì)它來說是沒有意義的。
所有 Ord 實(shí)例都可以使用 Data.List.sort 來排序。
幾乎所有 Haskell 內(nèi)置類型都是 Eq 類型類的實(shí)例,而 Ord 實(shí)例的類型也不在少數(shù)。
對(duì)于簡(jiǎn)單的數(shù)據(jù)類型, Haskell 編譯器可以自動(dòng)將類型派生(derivation)為 Read 、 Show 、 Bounded 、 Enum 、 Eq 和 Ord 的實(shí)例。
以下代碼將 Color 類型派生為 Read 、 Show 、 Eq 和 Ord 的實(shí)例:
data Color = Red | Green | Blue
deriving (Read, Show, Eq, Ord)
測(cè)試:
*Main> show Red
"Red"
*Main> (read "Red")::Color
Red
*Main> (read "[Red, Red, Blue]")::[Color]
[Red,Red,Blue]
*Main> Red == Red
True
*Main> Data.List.sort [Blue, Green, Blue, Red]
[Red,Green,Blue,Blue]
*Main> Red < Blue
True
注意 Color 類型的排序位置由定義類型時(shí)值構(gòu)造器的排序決定。
自動(dòng)派生并不總是可用的。比如說,如果定義類型 dataMyType=MyType(Int->Bool) ,那么編譯器就沒辦法派生 MyType 為 Show 的實(shí)例,因?yàn)樗恢涝撛趺磳?MyType 函數(shù)的輸出轉(zhuǎn)換成字符串,這會(huì)造成編譯錯(cuò)誤。
除此之外,當(dāng)使用自動(dòng)推導(dǎo)將某個(gè)類型設(shè)置為給定類型類的實(shí)例時(shí),定義這個(gè)類型時(shí)所使用的其他類型,也必須是給定類型類的實(shí)例(通過自動(dòng)推導(dǎo)或手動(dòng)添加的都可以)。
舉個(gè)例子,以下代碼不能使用自動(dòng)推導(dǎo):
data Book = Book
data BookInfo = BookInfo Book
deriving (Show)
Ghci 會(huì)給出提示,說明 Book 類型也必須是 Show 的實(shí)例, BookInfo 才能對(duì) Show 進(jìn)行自動(dòng)推導(dǎo):
Prelude> :load cant_ad.hs
[1 of 1] Compiling Main ( cant_ad.hs, interpreted )
ad.hs:4:27:
No instance for (Show Book)
arising from the 'deriving' clause of a data type declaration
Possible fix:
add an instance declaration for (Show Book)
or use a standalone 'deriving instance' declaration,
so you can specify the instance context yourself
When deriving the instance for (Show BookInfo)
Failed, modules loaded: none.
相反,以下代碼可以使用自動(dòng)推導(dǎo),因?yàn)樗鼘?duì) Book 類型也使用了自動(dòng)推導(dǎo),使得 Book 類型變成了 Show 的實(shí)例:
data Book = Book
deriving (Show)
data BookInfo = BookInfo Book
deriving (Show)
使用 :info 命令在 ghci 中確認(rèn)兩種類型都是 Show 的實(shí)例:
Prelude> :load ad.hs
[1 of 1] Compiling Main ( ad.hs, interpreted )
Ok, modules loaded: Main.
*Main> :info Book
data Book = Book -- Defined at ad.hs:1:6
instance Show Book -- Defined at ad.hs:2:23
*Main> :info BookInfo
data BookInfo = BookInfo Book -- Defined at ad.hs:4:6
instance Show BookInfo -- Defined at ad.hs:5:27
我們?cè)?在 Haskell 中表示 JSON 數(shù)據(jù) 一節(jié)介紹的 JValue 用起來還不夠簡(jiǎn)便。這里是一段由搜索引擎返回的實(shí)際 JSON 數(shù)據(jù)。刪除重整之后:
{
"query": "awkward squad haskell",
"estimatedCount": 3920,
"moreResults": true,
"results":
[{
"title": "Simon Peyton Jones: papers",
"snippet": "Tackling the awkward squad: monadic input/output ...",
"url": "http://research.microsoft.com/~simonpj/papers/marktoberdorf/",
},
{
"title": "Haskell for C Programmers | Lambda the Ultimate",
"snippet": "... the best job of all the tutorials I've read ...",
"url": "http://lambda-the-ultimate.org/node/724",
}]
}
進(jìn)一步簡(jiǎn)化之,并用 Haskell 表示:
-- file: ch06/SimpleResult.hs
import SimpleJSON
result :: JValue
result = JObject [
("query", JString "awkward squad haskell"),
("estimatedCount", JNumber 3920),
("moreResults", JBool True),
("results", JArray [
JObject [
("title", JString "Simon Peyton Jones: papers"),
("snippet", JString "Tackling the awkward ..."),
("url", JString "http://.../marktoberdorf/")
]])
]
由于 Haskell 不原生支持包含不同類型值的列表,我們不能直接表示包含不同類型值的 JSON 對(duì)象。我們需要把每個(gè)值都用 JValue 構(gòu)造器包裝起來。但這樣我們的靈活性就受到了限制:如果我們想把數(shù)字 3920 轉(zhuǎn)換成字符串 "3,920",我們就必須把 JNumber 構(gòu)造器換成 JString 構(gòu)造器。
Haskell 的類型類提供了一個(gè)誘人的解決方案:
-- file: ch06/JSONClass.hs
type JSONError = String
class JSON a where
toJValue :: a -> JValue
fromJValue :: JValue -> Either JSONError a
instance JSON JValue where
toJValue = id
fromJValue = Right
現(xiàn)在,我們無需再用 JNumber 等構(gòu)造器去包裝值了,直接使用 toJValue 函數(shù)即可。如果我們更改值的類型,編譯器會(huì)自動(dòng)選擇相應(yīng)的 toJValue 實(shí)現(xiàn)。
我們也提供了 fromJValue 函數(shù),它把 JValue 值轉(zhuǎn)換成我們希望的類型。
fromJValue 函數(shù)的返回類型為 Either。跟 Maybe 一樣,這個(gè)類型是預(yù)定義的。我們經(jīng)常用它來表示可能會(huì)失敗的計(jì)算。
雖然 Maybe 也用作這個(gè)目的,但它在錯(cuò)誤發(fā)生時(shí)沒有給我們足夠有用的信息:我們只得到一個(gè) Nothing。Either 類型的結(jié)構(gòu)相同,但它在錯(cuò)誤發(fā)生時(shí)會(huì)調(diào)用 Left 構(gòu)造器,并且還接受一個(gè)參數(shù)。
-- file: ch06/DataEither.hs
data Maybe a = Nothing
| Just a
deriving (Eq, Ord, Read, Show)
data Either a b = Left a
| Right b
deriving (Eq, Ord, Read, Show)
我們經(jīng)常使用 String 作為 a 參數(shù)的類型,以便在出錯(cuò)時(shí)提供有用的描述。為了說明在實(shí)際中怎么使用 Either 類型,我們來看一個(gè)簡(jiǎn)單實(shí)例。
-- file: ch06/JSONClass.hs
instance JSON Bool where
toJValue = JBool
fromJValue (JBool b) = Right b
fromJValue _ = Left "not a JSON boolean"
[譯注:讀者若想在 ghci 中嘗試 fromJValue,需要為其提供類型標(biāo)注,例如 (fromJValue(toJValueTrue))::EitherJSONErrorBool。]
Haskell 98標(biāo)準(zhǔn)不允許我們用下面的形式聲明實(shí)例,盡管它看起來沒什么問題:
-- file: ch06/JSONClass.hs
instance JSON String where
toJValue = JString
fromJValue (JString s) = Right s
fromJValue _ = Left "not a JSON string"
String 是 [Char] 的別名,因此它的類型是 [a],并用 Char 替換了類型變量 a。根據(jù) Haskell 98的規(guī)則,我們?cè)诼暶鲗?shí)例的時(shí)候不能用具體類型替代類型變量。也就是說,我們可以給 [a] 聲明實(shí)例,但給 [Char] 不行。
盡管 GHC 默認(rèn)遵守 Haskell 98標(biāo)準(zhǔn),但是我們可以在文件頂部添加特殊格式的注釋來解除這個(gè)限制。
-- file: ch06/JSONClass.hs
{-# LANGUAGE TypeSynonymInstances #-}
這條注釋是一條編譯器指令,稱為編譯選項(xiàng)(pragma),它告訴編譯器允許這項(xiàng)語(yǔ)言擴(kuò)展。上面的代碼因?yàn)?code>TypeSynonymInstances 這項(xiàng)語(yǔ)言擴(kuò)展而合法。我們?cè)诒菊拢ū緯┻€會(huì)碰到更多的語(yǔ)言擴(kuò)展。
[譯注:作者舉的這個(gè)例子實(shí)際上牽涉到了兩個(gè)問題。第一,Haskell 98不允許類型別名,這個(gè)問題可以通過上述方法解決。第二,Haskell 98不允許 [Char] 這種形式的類型,這個(gè)問題需要通過增加另外一條編譯選項(xiàng) {-#LANGUAGEFlexibleInstances#-} 來解決。]
Haskell 的設(shè)計(jì)允許我們?nèi)我鈩?chuàng)建類型類實(shí)例。
-- file: ch06/JSONClass.hs
doubleToJValue :: (Double -> a) -> JValue -> Either JSONError a
doubleToJValue f (JNumber v) = Right (f v)
doubleToJValue _ _ = Left "not a JSON number"
instance JSON Int where
toJValue = JNumber . realToFrac
fromJValue = doubleToJValue round
instance JSON Integer where
toJValue = JNumber . realToFrac
fromJValue = doubleToJValue round
instance JSON Double where
toJValue = JNumber
fromJValue = doubleToJValue id
我們可以在任意地方創(chuàng)建新實(shí)例,而不僅限于在定義了類型類的模塊中。類型類系統(tǒng)的這個(gè)特性被稱為開放世界假設(shè)(open world assumption)。如果有方法表示“這個(gè)類型類只存在這些實(shí)例”,那我們將得到一個(gè)封閉的世界。
我們希望把列表轉(zhuǎn)為 JSON 數(shù)組?,F(xiàn)在先不用關(guān)心實(shí)現(xiàn)細(xì)節(jié),暫時(shí)用 undefined 替代函數(shù)內(nèi)容即可。
-- file: ch06/BrokenClass.hs
instance (JSON a) => JSON [a] where
toJValue = undefined
fromJValue = undefined
我們也希望能將鍵/值對(duì)列表轉(zhuǎn)為 JSON 對(duì)象。
-- file: ch06/BrokenClass.hs
instance (JSON a) => JSON [(String, a)] where
toJValue = undefined
fromJValue = undefined
如果我們把這些定義放進(jìn)文件中并在 ghci 里載入,初看起來沒什么問題。
*JSONClass> :l BrokenClass.hs
[1 of 2] Compiling JSONClass ( JSONClass.hs, interpreted )
[2 of 2] Compiling BrokenClass ( BrokenClass.hs, interpreted )
Ok, modules loaded: JSONClass, BrokenClass
然而,當(dāng)我們使用序?qū)α斜韺?shí)例時(shí),麻煩來了。
*BrokenClass> toJValue [("foo","bar")]
<interactive>:10:1:
Overlapping instances for JSON [([Char], [Char])]
arising from a use of ‘toJValue’
Matching instances:
instance JSON a => JSON [(String, a)]
-- Defined at BrokenClass.hs:13:10
instance JSON a => JSON [a] -- Defined at BrokenClass.hs:8:10
In the expression: toJValue [("foo", "bar")]
In an equation for ‘it’: it = toJValue [("foo", "bar")]
重疊實(shí)例問題是由 Haskell 的開放世界假設(shè)造成的。 這里有一個(gè)更簡(jiǎn)單的例子來說明發(fā)生了什么。
-- file: ch06/Overlap.hs
class Borked a where
bork :: a -> String
instance Borked Int where
bork = show
instance Borked (Int, Int) where
bork (a, b) = bork a ++ ", " ++ bork b
instance (Borked a, Borked b) => Borked (a, b) where
bork (a, b) = ">>" ++ bork a ++ " " ++ bork b ++ "<<"
對(duì)于序?qū)?,我們有兩個(gè) Borked 類型類實(shí)例:一個(gè)是 Int 序?qū)?,另一個(gè)是任意類型的序?qū)?,只要這個(gè)類型是 Borked 類型類的實(shí)例。
假設(shè)我們想把 bork 應(yīng)用于 Int 序?qū)?。編譯器必須選擇一個(gè)實(shí)例來用。由于這兩個(gè)實(shí)例都能用,所以看上去它好像只要選那個(gè)更相關(guān)(specific)的實(shí)例就可以了。
但是,GHC 默認(rèn)是保守的。它堅(jiān)持只能有一個(gè)可用實(shí)例。這樣,當(dāng)我們?cè)噲D使用 bork 時(shí),它就會(huì)報(bào)錯(cuò)。
Note
重疊實(shí)例什么時(shí)候會(huì)出問題?
之前我們提到,我們可以把某個(gè)類型類的實(shí)例分散在幾個(gè)模塊中。GHC 并不會(huì)在意重疊實(shí)例的存在。相反,只有當(dāng)我們使用受影響的類型類的函數(shù),GHC 被迫要選擇使用哪個(gè)實(shí)例時(shí),它才會(huì)報(bào)錯(cuò)。
通常,我們不能給多態(tài)類型(polymorphic type)的特化版本(specialized version)寫類型類實(shí)例。[Char] 類型就是多態(tài)類型 [a] 特化成 Char 的結(jié)果。因此我們禁止聲明 [Char] 為某個(gè)類型類的實(shí)例。這非常不方便,因?yàn)樽址诖a中無處不在。
FlexibleInstances 語(yǔ)言擴(kuò)展取消了這個(gè)限制,它允許我們寫這樣的實(shí)例。
GHC 支持另外一個(gè)有用的語(yǔ)言擴(kuò)展,OverlappingInstances,它解決了重疊實(shí)例帶來的問題。如果存在重疊實(shí)例,編譯器會(huì)選擇最相關(guān)的(specific)那一個(gè)。
我們經(jīng)常把這個(gè)擴(kuò)展和 TypeSynonymInstances 放在一起使用。下面是一個(gè)例子。
-- file: ch06/SimpleClass.hs
{-# LANGUAGE TypeSynonymInstances, OverlappingInstances #-}
import Data.List
class Foo a where
foo :: a -> String
instance Foo a => Foo [a] where
foo = concat . intersperse ", " . map foo
instance Foo Char where
foo c = [c]
instance Foo String where
foo = id
如果我們對(duì) String 應(yīng)用 foo,編譯器會(huì)選擇 String 的特定實(shí)現(xiàn)。即使 [a] 和 Char 都是 Foo 的實(shí)例,但由于 String 實(shí)例更相關(guān),因此 GHC 選擇了它。
即使開了 OverlappingInstances 擴(kuò)展,如果 GHC 發(fā)現(xiàn)了多個(gè)同樣相(equally specific)關(guān)的實(shí)例,它仍然會(huì)拒絕代碼。
何時(shí)使用 OverlappingInstances 擴(kuò)展(to be added)
OverlappingInstances 和 TypeSynonymInstances 語(yǔ)言擴(kuò)展是 GHC 特有的,Haskell 98 并不支持。然而,Haskell 98 中的 Show 類型類在轉(zhuǎn)化 Char 列表和 Int 列表時(shí)卻用了不同的方法。它用了一個(gè)聰明但簡(jiǎn)單的小技巧。
Show 類型類定義了轉(zhuǎn)換單個(gè)值的 show 方法和轉(zhuǎn)換列表的 showList 方法。showList 默認(rèn)使用中括號(hào)和逗號(hào)轉(zhuǎn)換列表。
[a] 的 Show 實(shí)例使用 showList 實(shí)現(xiàn)。Char 的 Show 實(shí)例提供了一個(gè)特殊的 showList 實(shí)現(xiàn),它使用雙引號(hào),并轉(zhuǎn)義非 ASCII 打印字符。
結(jié)果是,如果有人想對(duì) [Char] 應(yīng)用 show,編譯器會(huì)選擇 showList 的實(shí)現(xiàn),并使用雙引號(hào)正確轉(zhuǎn)換這個(gè)字符串。
這樣,換個(gè)角度看問題,我們就能避免 OverlappingInstances 擴(kuò)展了。
除了熟悉的 data 關(guān)鍵字外,Haskell 還允許我們用 newtype 關(guān)鍵字來創(chuàng)建新類型。
-- file: ch06/Newtype.hs
data DataInt = D Int
deriving (Eq, Ord, Show)
newtype NewtypeInt = N Int
deriving (Eq, Ord, Show)
newtype 聲明的作用是重命名現(xiàn)有類型,并給它一個(gè)新身份??梢钥闯?,它的用法和使用 data 關(guān)鍵字進(jìn)行類型聲明看起來很相似。
Note
type 和 newtype 關(guān)鍵字
盡管名字類似,type 和 newtype 關(guān)鍵字的作用卻完全不同。type 關(guān)鍵字給了我們另一種指代某個(gè)類型的方法,類似于給朋友起的綽號(hào)。我們和編譯器都知道 [Char] 和 String 指的是同一個(gè)類型。
相反,newtype 關(guān)鍵字的存在是為了隱藏類型的本性??紤]這個(gè) UniqueID 類型。
-- file: ch06/Newtype.hs
newtype UniqueID = UniqueID Int
deriving (Eq)
編譯器會(huì)把 UniqueID 當(dāng)成和 Int 不同的類型。作為 UniqueID 的用戶,我們只知道它是一個(gè)唯一標(biāo)識(shí)符;我們并不知道它是用 Int 來實(shí)現(xiàn)的。
在聲明 newtype 時(shí),我們必須決定暴露被重命名類型的哪些類型類實(shí)例。這里,我們讓 NewtypeInt 提供 Int 類型的 Eq, Ord 和 Show 實(shí)例。這樣,我們就可以比較和打印 NewtypeInt 類型的值了。
*Main> N 1 < N 2
True
由于我們沒有暴露 Int 的 Num 或 Integral 實(shí)例,NewtypeInt 類型的值并不是數(shù)字。例如,我們不能做加法。
*Main> N 313 + N 37
<interactive>:9:7:
No instance for (Num NewtypeInt) arising from a use of ‘+’
In the expression: N 313 + N 37
In an equation for ‘it’: it = N 313 + N 37
跟用 data 關(guān)鍵字一樣,我們可以用 newtype 的值構(gòu)造器創(chuàng)建新值,或者對(duì)現(xiàn)有值進(jìn)行模式匹配。 如果 newtype 沒用自動(dòng)派生來暴露對(duì)應(yīng)類型的類型類實(shí)現(xiàn)的話,我們可以自己寫一個(gè)新實(shí)例或者干脆不實(shí)現(xiàn)那個(gè)類型類。 data 和 newtype 的區(qū)別 newtype 關(guān)鍵字給現(xiàn)有類型一個(gè)不同的身份,相比起 data,它使用時(shí)的限制更多。具體來講,newtype 只能有一個(gè)值構(gòu)造器, 并且這個(gè)構(gòu)造器只能有一個(gè)字段。
-- file: ch06/NewtypeDiff.hs
-- 可以:任意數(shù)量的構(gòu)造器和字段
data TwoFields = TwoFields Int Int
-- 可以:一個(gè)字段
newtype Okay = ExactlyOne Int
-- 可以:使用類型變量
newtype Param a b = Param (Either a b)
-- 可以:使用記錄語(yǔ)法
newtype Record = Record {
getInt :: Int
}
-- 不可以:沒有字段
newtype TooFew = TooFew
-- 不可以:多于一個(gè)字段
newtype TooManyFields = Fields Int Int
-- 不可以:多于一個(gè)構(gòu)造器
newtype TooManyCtors = Bad Int
| Worse Int
除此之外,data 和 newtype 還有一個(gè)重要區(qū)別。由 data 關(guān)鍵字創(chuàng)建的類型在運(yùn)行時(shí)有一個(gè)簿記開銷,如記錄某個(gè)值是用哪個(gè)構(gòu)造器創(chuàng)建的。而 newtype 只有一個(gè)構(gòu)造器,所以不需要這個(gè)額外開銷。這使得它在運(yùn)行時(shí)更省時(shí)間和空間。
由于 newtype 的構(gòu)造器只在編譯時(shí)使用,運(yùn)行時(shí)甚至不存在,用 newtype 定義的類型和用 data 定義的類型在匹配 undefined 時(shí)會(huì)有不同的行為。
為了理解它們的不同點(diǎn),我們首先回顧一下普通數(shù)據(jù)類型的行為。我們已經(jīng)非常熟悉,在運(yùn)行時(shí)對(duì) undefined 求值會(huì)導(dǎo)致崩潰。
Prelude> undefined
*** Exception: Prelude.undefined
我們把 undefined 放進(jìn) D 構(gòu)造器創(chuàng)建一個(gè) DataInt,然后對(duì)它進(jìn)行模式匹配。
*Main> case (D undefined) of D _ -> 1
1
由于我們的模式匹配只匹配構(gòu)造器而不管里面的值,undefined 未被求值,因而不會(huì)拋出異常。
下面的例子沒有使用 D 構(gòu)造器,因而模式匹配時(shí) undefined 被求值,異常拋出。
*Main> case undefined of D _ -> 1
*** Exception: Prelude.undefined
當(dāng)我們用 N 構(gòu)造器創(chuàng)建 NewtypeInt 值時(shí),它的行為與使用 DataInt 類型的 D 構(gòu)造器相同:沒有異常。
*Main> case (N undefined) of N _ -> 1
1
但當(dāng)我們把表達(dá)式中的 N 去掉,并對(duì) undefined 進(jìn)行模式匹配時(shí),關(guān)鍵的不同點(diǎn)來了。
*Main> case undefined of N _ -> 1
1
沒有崩潰!由于運(yùn)行時(shí)不存在構(gòu)造器,匹配 N 實(shí)際上就是在匹配通配符 :由于通配符總可以被匹配,所以表達(dá)式是不需要被求值的。
這里簡(jiǎn)要回顧一下 haskell 引入新類型名的三種方式。
Haskell 98 有一個(gè)微妙的特性可能會(huì)在某些意想不到的情況下“咬”到我們。下面這個(gè)簡(jiǎn)單的函數(shù)展示了這個(gè)問題。
-- file: ch06/Monomorphism.hs
myShow = show
如果我們?cè)噲D把它載入 ghci,會(huì)產(chǎn)生一個(gè)奇怪的錯(cuò)誤:
Prelude> :l Monomorphism.hs
[1 of 1] Compiling Main ( Monomorphism.hs, interpreted )
Monomorphism.hs:2:10:
No instance for (Show a0) arising from a use of ‘show’
The type variable ‘a(chǎn)0’ is ambiguous
Relevant bindings include
myShow :: a0 -> String (bound at Monomorphism.hs:2:1)
Note: there are several potential instances:
instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
instance Show Ordering -- Defined in ‘GHC.Show’
instance Show Integer -- Defined in ‘GHC.Show’
...plus 22 others
In the expression: show
In an equation for ‘myShow’: myShow = show
Failed, modules loaded: none.
[譯注:譯者得到的輸出和原文有出入,這里提供的是使用最新版本 GHC 得到的輸出。] 錯(cuò)誤信息中提到的 “monomorphism” 是 Haskell 98 的一部分。 單一同態(tài)是多態(tài)(polymorphism)的反義詞:它表明某個(gè)表達(dá)式只有一種類型。 Haskell 有時(shí)會(huì)強(qiáng)制使某些聲明不像我們預(yù)想的那么多態(tài)。 我們?cè)谶@里提單一同態(tài)是因?yàn)楸M管它和類型類沒有直接關(guān)系,但類型類給它提供了產(chǎn)生的環(huán)境。 Note 在實(shí)際代碼中可能很久都不會(huì)碰到單一同態(tài),因此我們覺得你沒必要記住這部分的細(xì)節(jié), 只要在心里知道有這么回事就可以了,除非 GHC 真的報(bào)告了跟上面類似的錯(cuò)誤。 如果真的發(fā)生了,記得在這兒曾讀過這個(gè)錯(cuò)誤,然后回過頭來看就行了。 我們不會(huì)試圖去解釋單一同態(tài)限制。Haskell 社區(qū)一致同意它并不經(jīng)常出現(xiàn);它解釋起來很棘手(tricky); 它幾乎沒什么實(shí)際用處;它唯一的作用就是坑人。舉個(gè)例子來說明它為什么棘手:盡管上面的例子違反了這個(gè)限制, 下面的兩個(gè)編譯起來卻毫無問題。
-- file: ch06/Monomorphism.hs
myShow2 value = show value
myShow3 :: (Show a) => a -> String
myShow3 = show
上面的定義表明,如果 GHC 報(bào)告單一同態(tài)限制錯(cuò)誤,我們有三個(gè)簡(jiǎn)單的方法來處理。
沒人喜歡單一同態(tài)限制,因此幾乎可以肯定的是下一個(gè)版本的 Haskell 會(huì)去掉它。但這并不是說加上 NoMonomorphismRestriction 就可以一勞永逸:有些編譯器(包括一些老版本的 GHC)識(shí)別不了這個(gè)擴(kuò)展,但用另外兩種方法就可以解決問題。如果這種可移植性對(duì)你不是問題,那么請(qǐng)務(wù)必打開這個(gè)擴(kuò)展。
在這章,你學(xué)到了類型類有什么用以及怎么用它們。我們討論了如何定義自己的類型類,然后又討論了一些 Haskell 庫(kù)里定義的類型類。最后,我們展示了怎么讓 Haskell 編譯器給你的類型自動(dòng)派生出某些類型類實(shí)例。
更多建議: