第六章:類型類

2018-02-24 15:49 更新

第六章:類型類

類型類(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)用,直到程序崩潰。

定義類型類實(shí)例

定義一個(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 類型類的信息。

幾個(gè)重要的內(nèi)置類型類

前面兩節(jié)分別介紹了類型類的定義,以及如何讓某個(gè)類型成為給定類型類的實(shí)例類型。

正本節(jié)會(huì)介紹幾個(gè) Prelude 庫(kù)中包含的類型類。如本章開始時(shí)所說的,類型類是 Haskell 語(yǔ)言某些特性的奠基石,本節(jié)就會(huì)介紹幾個(gè)這方面的例子。

更多信息可以參考 Haskell 的函數(shù)參考,那里一般都給出了類型類的詳細(xì)介紹,并且說明,要成為這個(gè)類型類的實(shí)例,需要實(shí)現(xiàn)那些函數(shù)。

Show

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

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ù)返回正確類型的值,必須給它指示正確的類型。

使用 Read 和 Show 進(jìn)行序列化

很多時(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è)列表。

數(shù)字類型

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, FloatInt, WordIntegerRational
Double, FloatInt, WordIntegerRationalfromRational . toRationalfromIntegralfromIntegralfromRationaltruncate *fromIntegralfromIntegraltruncate *truncate *fromIntegralN/Atruncate *toRationalfromIntegralfromIntegralN/A
  • 除了 truncate 之外,還可以使用 round 、 ceiling 或者 float 。

第十三章會(huì)說明,怎樣用自定義數(shù)據(jù)類型來擴(kuò)展數(shù)字類型。

相等性,有序和對(duì)比

除了前面介紹的通用算術(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ù)。

自動(dòng)派生

對(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

類型類實(shí)戰(zhàn):讓 JSON 更好用

我們?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)換成我們希望的類型。

讓錯(cuò)誤信息更有用

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。]

使用類型別名創(chuàng)建實(shí)例

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

什么時(shí)候重疊實(shí)例(Overlapping instances)會(huì)出問題?

如果我們把這些定義放進(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)

字符串的 show 是如何工作的?

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ò)展了。

如何給類型定義新身份(Identity)

除了熟悉的 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 引入新類型名的三種方式。

  • data 關(guān)鍵字定義一個(gè)真正的代數(shù)數(shù)據(jù)類型。
  • type 關(guān)鍵字給現(xiàn)有類型定義別名。類型和別名可以通用。
  • newtype 關(guān)鍵字給現(xiàn)有類型定義一個(gè)不同的身份(distinct identity)。原類型和新類型不能通用。

JSON typeclasses without overlapping instances

可怕的單一同態(tài)限定(monomorphism restriction)

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)單的方法來處理。

  • 顯式聲明函數(shù)參數(shù),而不是隱性。
  • 顯式定義類型簽名,而不是依靠編譯器去推導(dǎo)。
  • 不改代碼,編譯模塊的時(shí)候用上 NoMonomorphismRestriction 語(yǔ)言擴(kuò)展。它取消了單一同態(tài)限制。

沒人喜歡單一同態(tài)限制,因此幾乎可以肯定的是下一個(gè)版本的 Haskell 會(huì)去掉它。但這并不是說加上 NoMonomorphismRestriction 就可以一勞永逸:有些編譯器(包括一些老版本的 GHC)識(shí)別不了這個(gè)擴(kuò)展,但用另外兩種方法就可以解決問題。如果這種可移植性對(duì)你不是問題,那么請(qǐng)務(wù)必打開這個(gè)擴(kuò)展。

結(jié)論

在這章,你學(xué)到了類型類有什么用以及怎么用它們。我們討論了如何定義自己的類型類,然后又討論了一些 Haskell 庫(kù)里定義的類型類。最后,我們展示了怎么讓 Haskell 編譯器給你的類型自動(dòng)派生出某些類型類實(shí)例。

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)