<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                # 第六章:類型類 類型類(typeclass)是 Haskell 最強大的功能之一:它用于定義通用接口,為各種不同的類型提供一組公共特性集。 類型類是某些基本語言特性的核心,比如相等性測試和數值操作符。 在討論如何使用類型類之前,先來看看它能做什么。 ## 類型類的作用 假設這樣一個場景:我們想對 Color 類型的值進行對比,但 Haskell 的語言設計者卻沒有實現 == 操作。 要解決這個問題,必須親自實現一個相等性測試函數: ~~~ -- 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 里測試: ~~~ 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 ~~~ 過了一會,程序又添加了一個新類型 —— 職位:它對公司中的各個員工進行分類。 在執行像是工資計算這類任務是,又需要用到相等性測試,所以又需要再次為職位類型定義相等性測試函數: ~~~ -- 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 ~~~ 測試: ~~~ 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 的定義揭示了一個問題:對于每個不同的類型,我們都需要為它們專門定義一個對比函數。 這種做法非常低效,而且煩人。如果同一個對比函數(比如 == )可以用于對比任何類型的值,這樣就會方便得多。 另一方面,一般來說,如果定義了相等測試函數(比如 == ),那么不等測試函數(比如 /= )的值就可以直接對相等測試函數取反(使用 not )來計算得出。因此,如果可以通過相等測試函數來定義不等測試函數,那么會更方便。 通用函數還可以讓代碼變得更通用:如果同一段代碼可以用于不同類型的輸入值,那么程序的代碼量將大大減少。 還有很重要的一點是,如果在之后添加通用函數對新類型的支持,那么原來的代碼應該不需要進行修改。 Haskell 的類型類可以滿足以上提到的所有要求。 ## 什么是類型類? 類型類定義了一系列函數,這些函數對于不同類型的值使用不同的函數實現。它和其他語言的接口和多態方法有些類似。 [譯注:這里原文是將“面向對象編程中的對象”和 Haskell 的類型類進行類比,但實際上這種類比并不太恰當,類比成接口和多態方法更適合一點。] 我們定義一個類型類來解決前面提到的相等性測試問題: ~~~ class BasicEq a where isEqual :: a -> a -> Bool ~~~ 類型類使用 class 關鍵字來定義,跟在 class 之后的 BasicEq 是這個類型類的名字,之后的 a 是這個類型類的實例類型(instance type)。 BasicEq 使用類型變量 a 來表示實例類型,說明它并不將這個類型類限定于某個類型:任何一個類型,只要它實現了這個類型類中定義的函數,那么它就是這個類型類的實例類型。 實例類型所使用的名字可以隨意選擇,但是它和類型類中定義函數簽名時所使用的名字應該保持一致。比如說,我們使用 a 來表示實例類型,那么函數簽名中也必須使用 a 來代表這個實例類型。 BasicEq 類型類只定義了 isEqual 一個函數 —— 它接受兩個參數作為輸入,并且這兩個參數都指向同一種實例類型: ~~~ 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 的實例類型,實現了 isEqual 函數: ~~~ instance BasicEq Bool where isEqual True True = True isEqual False False = True isEqual _ _ = False ~~~ 在 ghci 里驗證這個程序: ~~~ *Main> isEqual True True True *Main> isEqual False True False ~~~ 如果試圖將不是 BasicEq 實例類型的值作為輸入調用 isEqual 函數,那么就會引發錯誤: ~~~ *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" ~~~ 錯誤信息提醒我們, [Char] 并不是 BasicEq 的實例類型。 稍后的一節會介紹更多關于類型類實例的定義方式,這里先繼續前面的例子。這一次,除了 isEqual 之外,我們還想定義不等測試函數 isNotEqual : ~~~ class BasicEq a where isEqual :: a -> a -> Bool isNotEqual :: a -> a -> Bool ~~~ 同時定義 isEqual 和 isNotEqual 兩個函數產生了一些不必要的工作:從邏輯上講,對于任何類型,只要知道 isEqual 或 isNotEqual 的任意一個,就可以計算出另外一個。因此,一種更省事的辦法是,為 isEqual 和 isNotEqual 兩個函數提供默認值,這樣 BasicEq 的實例類型只要實現這兩個函數中的一個,就可以順利使用這兩個函數: ~~~ 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 實例類型的例子: ~~~ instance BasicEq Bool where isEqual False False = True isEqual True True = True isEqual _ _ = False ~~~ 我們只要定義 isEqual 函數,就可以“免費”得到 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 ~~~ 當然,如果閑著沒事,你仍然可以自己親手定義這兩個函數。但是,你至少要定義兩個函數中的一個,否則兩個默認的函數就會互相調用,直到程序崩潰。 ## 定義類型類實例 定義一個類型為某個類型類的實例,指的就是,為某個類型實現給定類型類所聲明的全部函數。 比如在前面, BasicEq 類型類定義了兩個函數 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) ~~~ 在前一節,我們成功將 Bool 類型實現為 BasicEq 的實例類型,要使 Color 類型也成為 BasicEq 類型類的實例,就需要另外為 Color 類型實現 isEqual 和 isNotEqual : ~~~ instance BasicEq Color where isEqual Red Red = True isEqual Blue Blue = True isEqual Green Green = True isEqual _ _ = True ~~~ 注意,這里的函數定義和之前的 colorEq 函數定義實際上沒有什么不同,唯一的區別是,它使得 isEqual 不僅可以對 Bool 類型進行對比測試,還可以對 Color 類型進行對比測試。 更一般地說,只要為相應的類型實現 BasicEq 類型類中的定義,那么 isEqual 就可以用于對比*任何*我們想對比的類型。 不過在實際中,通常并不使用 BasicEq 類型類,而是使用 Haskell Report 中定義的 Eq 類型類:它定義了 == 和 /= 操作符,這兩個操作符才是 Haskell 中最常用的測試函數。 以下是 Eq 類型類的定義: ~~~ class Eq a where (==), (/=) :: a -> a -> Bool -- Minimal complete definition: -- (==) or (/=) x /= y = not (x == y) x == y = not (x /= y) ~~~ 稍后會介紹更多使用 Eq 類型類的信息。 ## 幾個重要的內置類型類 前面兩節分別介紹了類型類的定義,以及如何讓某個類型成為給定類型類的實例類型。 正本節會介紹幾個 Prelude 庫中包含的類型類。如本章開始時所說的,類型類是 Haskell 語言某些特性的奠基石,本節就會介紹幾個這方面的例子。 更多信息可以參考 Haskell 的函數參考,那里一般都給出了類型類的詳細介紹,并且說明,要成為這個類型類的實例,需要實現那些函數。 ## Show Show 類型類用于將值轉換為字符串,它最重要的函數是 show 。 show 函數使用單個參數接收輸入數據,并返回一個表示該輸入數據的字符串: ~~~ Main> :type show show :: Show a => a -> String ~~~ 以下是一些 show 函數調用的例子: ~~~ Main> show 1 "1" Main> show [1, 2, 3] "[1,2,3]" Main> show (1, 2) "(1,2)" ~~~ Ghci 輸出一個值,實際上就是對這個值調用 putStrLn 和 show : ~~~ Main> 1 1 Main> show 1 "1" Main> putStrLn (show 1) 1 ~~~ 因此,如果你定義了一種新的數據類型,并且希望通過 ghci 來顯示它,那么你就應該將這個類型實現為 Show 類型類的實例,否則 ghci 就會向你抱怨,說它不知道該怎樣用字符串的形式表示這種數據類型: ~~~ 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 ~~~ 通過實現 Color 類型的 show 函數,讓 Color 類型成為 Show 的類型實例,可以解決以上問題: ~~~ instance Show Color where show Red = "Red" show Green = "Green" show Blue = "Blue" ~~~ 當然, show 函數的打印值并不是非要和類型構造器一樣不可,比如 Red 值并不是非要表示為 "Red" 不可,以下是另一種實例化 Show 類型類的方式: ~~~ instance Show Color where show Red = "Color 1: Red" show Green = "Color 2: Green" show Blue = "Color 3: Blue" ~~~ ## Read Read 和 Show 類型類的作用正好相反,它將字符串轉換為值。 Read 最有用的函數是 read :它接受一個字符串作為參數,對這個字符串進行處理,并返回一個值,這個值的類型為 Read 實例類型的成員(所有實例類型中的一種)。 ~~~ 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 ~~~ 注意在第一次調用 read 的時候,我們并沒有顯式地給定類型簽名,這時對 read"3" 的求值會引發錯誤。這是因為有非常多的類型都是 Read 的實例,而編譯器在 read 函數讀入 "3" 之后,不知道應該將這個值轉換成什么類型,于是編譯器就會向我們發牢騷。 因此,為了讓 read 函數返回正確類型的值,必須給它指示正確的類型。 ## 使用 Read 和 Show 進行序列化 很多時候,程序需要將內存中的數據保存為文件,又或者,反過來,需要將文件中的數據轉換為內存中的數據實體。這種轉換過程稱為*序列化*和*反序列化* . 通過將類型實現為 Read 和 Show 的實例類型, read 和 show 兩個函數可以成為非常好的序列化工具。 作為例子,以下代碼將一個內存中的列表序列化到文件中: ~~~ Prelude> let years = [1999, 2010, 2012] Prelude> show years "[1999,2010,2012]" Prelude> writeFile "years.txt" (show years) ~~~ writeFile 將給定內容寫入到文件當中,它接受兩個參數,第一個參數是文件路徑,第二個參數是寫入到文件的字符串內容。 觀察文件 years.txt 可以看到, (showyears) 所產生的文本被成功保存到了文件當中: ~~~ $ cat years.txt [1999,2010,2012] ~~~ 使用以下代碼可以對 years.txt 進行反序列化操作: ~~~ Prelude> input <- readFile "years.txt" Prelude> input -- 讀入的字符串 "[1999,2010,2012]" Prelude> (read input)::[Int] -- 將字符串轉換成列表 [1999,2010,2012] ~~~ readFile 讀入給定的 years.txt ,并將它的內存傳給 input 變量,最后,通過使用 read ,我們成功將字符串反序列化成一個列表。 ## 數字類型 Haskell 有一集非常強大的數字類型:從速度飛快的 32 位或 64 位整數,到任意精度的有理數,包羅萬有。 除此之外,Haskell 還有一系列通用算術操作符,這些操作符可以用于幾乎所有數字類型。而對數字類型的這種強有力的支持就是建立在類型類的基礎上的。 作為一個額外的好處(side benefit),用戶可以定義自己的數字類型,并且獲得和內置數字類型完全平等的權利。 以下表格顯示了 Haskell 中最常用的一些數字類型: **表格 6.1 : 部分數字類型** | 類型 | 介紹 | |-----|-----| | Double | 雙精度浮點數。表示浮點數的常見選擇。 | | Float | 單精度浮點數。通常在對接 C 程序時使用。 | | Int | 固定精度帶符號整數;最小范圍在 -2^29 至 2^29-1 。相當常用。 | | Int8 | 8 位帶符號整數 | | Int16 | 16 位帶符號整數 | | Int32 | 32 位帶符號整數 | | Int64 | 64 位帶符號整數 | | Integer | 任意精度帶符號整數;范圍由機器的內存限制。相當常用。 | | Rational | 任意精度有理數。保存為兩個整數之比(ratio)。 | | Word | 固定精度無符號整數。占用的內存大小和 Int 相同 | | Word8 | 8 位無符號整數 | | Word16 | 16 位無符號整數 | | Word32 | 32 位無符號整數 | | Word64 | 64 位無符號整數 | 大部分算術操作都可以用于任意數字類型,少數的一部分函數,比如 asin ,只能用于浮點數類型。 以下表格列舉了操作各種數字類型的常見函數和操作符: **表格 6.2 : 部分數字函數和** | 項 | 類型 | 模塊 | 描述 | |-----|-----|-----|-----| | (+) | Num a => a -> a -> a | Prelude | 加法 | | (-) | Num a => a -> a -> a | Prelude | 減法 | | (*) | Num a => a -> a -> a | Prelude | 乘法 | | (/) | Fractional a => a -> a -> a | Prelude | 份數除法 | | (**) | Floating a => a -> a -> a | Prelude | 乘冪 | | (^) | (Num a, Integral b) => a -> b -> a | Prelude | 計算某個數的非負整數次方 | | (^^) | (Fractional a, Integral b) => a -> b -> a | Prelude | 分數的任意整數次方 | | (%) | Integral a => a -> a -> Ratio a | Data.Ratio | 構成比率 | | (.&.) | Bits a => a -> a -> a | Data.Bits | 二進制并操作 | | (.|.) | Bits a => a -> a -> a | Data.Bits | 二進制或操作 | | abs | Num a => a -> a | Prelude | 絕對值操作 | | approxRational | RealFrac a => a -> a -> Rational | Data.Ratio | 通過分數的分子和分母計算出近似有理數 | | cos | Floating a => a -> a | Prelude | 余弦函數。另外還有 acos 、 cosh 和 acosh ,類型和 cos 一樣。 | | div | Integral a => a -> a -> a | Prelude | 整數除法,總是截斷小數位。 | | fromInteger | Num a => Integer -> a | Prelude | 將一個 Integer 值轉換為任意數字類型。 | | fromIntegral | (Integral a, Num b) => a -> b | Prelude | 一個更通用的轉換函數,將任意 Integral 值轉為任意數字類型。 | | fromRational | Fractional a => Rational -> a | Prelude | 將一個有理數轉換為分數。可能會有精度損失。 | | log | Floating a => a -> a | Prelude | 自然對數算法。 | | logBase | Floating a => a -> a -> a | Prelude | 計算指定底數對數。 | | maxBound | Bounded a => a | Prelude | 有限長度數字類型的最大值。 | | minBound | Bounded a => a | Prelude | 有限長度數字類型的最小值。 | | mod | Integral a => a -> a -> a | Prelude | 整數取模。 | | pi | Floating a => a | Prelude | 圓周率常量。 | | quot | Integral a => a -> a -> a | Prelude | 整數除法;商數的分數部分截斷為 0 。 | | recip | Fractional a => a -> a | Prelude | 分數的倒數。 | | rem | Integral a => a -> a -> a | Prelude | 整數除法的余數。 | | round | (RealFrac a, Integral b) => a -> b | Prelude | 四舍五入到最近的整數。 | | shift | Bits a => a -> Int -> a | Bits | 輸入為正整數,就進行左移。如果為負數,進行右移。 | | sin | Floating a => a -> a | Prelude | 正弦函數。還提供了 asin 、 sinh 和 asinh ,和 sin 類型一樣。 | | sqrt | Floating a => a -> a | Prelude | 平方根 | | tan | Floating a => a -> a | Prelude | 正切函數。還提供了 atan 、 tanh 和 atanh ,和 tan 類型一樣。 | | toInteger | Integral a => a -> Integer | Prelude | 將任意 Integral 值轉換為 Integer | | toRational | Real a => a -> Rational | Prelude | 從實數到有理數的有損轉換 | | truncate | (RealFrac a, Integral b) => a -> b | Prelude | 向下取整 | | xor | Bits a => a -> a -> a | Data.Bits | 二進制異或操作 | 數字類型及其對應的類型類列舉在下表: **表格 6.3 : 數字類型的類型類實例** | 類型 | 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 列舉了一些數字類型之間進行轉換的函數,以下表格是一個匯總: **表格 6.4 : 數字類型之間的轉換** <table border="1" class="docutils"><colgroup><col width="15%"/><col width="29%"/><col width="15%"/><col width="16%"/><col width="24%"/></colgroup><tbody valign="top"><tr class="row-odd"><td rowspan="2">源類型</td><td colspan="4">目標類型</td></tr><tr class="row-even"><td>Double, Float</td><td>Int, Word</td><td>Integer</td><td>Rational</td></tr><tr class="row-odd"><td>Double, FloatInt, WordIntegerRational</td><td>fromRational . toRationalfromIntegralfromIntegralfromRational</td><td>truncate *fromIntegralfromIntegraltruncate *</td><td>truncate *fromIntegralN/Atruncate *</td><td>toRationalfromIntegralfromIntegralN/A</td></tr></tbody></table> * 除了 truncate 之外,還可以使用 round 、 ceiling 或者 float 。 第十三章會說明,怎樣用自定義數據類型來擴展數字類型。 ## 相等性,有序和對比 除了前面介紹的通用算術符號之外,相等測試、不等測試、大于和小于等對比操作也是非常常見的。 其中, Eq 類型類定義了 == 和 /= 操作,而 >= 和 <= 等對比操作,則由 Ord 類型類定義。 需要將對比操作和相等性測試分開用兩個類型類來定義的原因是,對于某些類型,它們只對相等性測試和不等測試有興趣,比如 Handle 類型,而部分有序操作(particular ordering, 大于、小于等)對它來說是沒有意義的。 所有 Ord 實例都可以使用 Data.List.sort 來排序。 幾乎所有 Haskell 內置類型都是 Eq 類型類的實例,而 Ord 實例的類型也不在少數。 ## 自動派生 對于簡單的數據類型, Haskell 編譯器可以自動將類型派生(derivation)為 Read 、 Show 、 Bounded 、 Enum 、 Eq 和 Ord 的實例。 以下代碼將 Color 類型派生為 Read 、 Show 、 Eq 和 Ord 的實例: ~~~ data Color = Red | Green | Blue deriving (Read, Show, Eq, Ord) ~~~ 測試: ~~~ *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 類型的排序位置由定義類型時值構造器的排序決定。 自動派生并不總是可用的。比如說,如果定義類型 dataMyType=MyType(Int->Bool) ,那么編譯器就沒辦法派生 MyType 為 Show 的實例,因為它不知道該怎么將 MyType 函數的輸出轉換成字符串,這會造成編譯錯誤。 除此之外,當使用自動推導將某個類型設置為給定類型類的實例時,定義這個類型時所使用的其他類型,也必須是給定類型類的實例(通過自動推導或手動添加的都可以)。 舉個例子,以下代碼不能使用自動推導: ~~~ data Book = Book data BookInfo = BookInfo Book deriving (Show) ~~~ Ghci 會給出提示,說明 Book 類型也必須是 Show 的實例, BookInfo 才能對 Show 進行自動推導: ~~~ 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. ~~~ 相反,以下代碼可以使用自動推導,因為它對 Book 類型也使用了自動推導,使得 Book 類型變成了 Show 的實例: ~~~ data Book = Book deriving (Show) data BookInfo = BookInfo Book deriving (Show) ~~~ 使用 :info 命令在 ghci 中確認兩種類型都是 Show 的實例: ~~~ 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 ~~~ ## 類型類實戰:讓 JSON 更好用 我們在 [*在 Haskell 中表示 JSON 數據*](#) 一節介紹的 JValue 用起來還不夠簡便。這里是一段由搜索引擎返回的實際 JSON 數據。刪除重整之后: ~~~ { "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", }] } ~~~ 進一步簡化之,并用 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 對象。我們需要把每個值都用 JValue 構造器包裝起來。但這樣我們的靈活性就受到了限制:如果我們想把數字 3920 轉換成字符串 "3,920",我們就必須把 JNumber 構造器換成 JString 構造器。 Haskell 的類型類提供了一個誘人的解決方案: ~~~ -- 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 ~~~ 現在,我們無需再用 JNumber 等構造器去包裝值了,直接使用 toJValue 函數即可。如果我們更改值的類型,編譯器會自動選擇相應的 toJValue 實現。 我們也提供了 fromJValue 函數,它把 JValue 值轉換成我們希望的類型。 ## 讓錯誤信息更有用 fromJValue 函數的返回類型為 Either。跟 Maybe 一樣,這個類型是預定義的。我們經常用它來表示可能會失敗的計算。 雖然 Maybe 也用作這個目的,但它在錯誤發生時沒有給我們足夠有用的信息:我們只得到一個 Nothing。Either 類型的結構相同,但它在錯誤發生時會調用 Left 構造器,并且還接受一個參數。 ~~~ -- 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) ~~~ 我們經常使用 String 作為 a 參數的類型,以便在出錯時提供有用的描述。為了說明在實際中怎么使用 Either 類型,我們來看一個簡單實例。 ~~~ -- file: ch06/JSONClass.hs instance JSON Bool where toJValue = JBool fromJValue (JBool b) = Right b fromJValue _ = Left "not a JSON boolean" ~~~ [譯注:讀者若想在 **ghci** 中嘗試 fromJValue,需要為其提供類型標注,例如 (fromJValue(toJValueTrue))::EitherJSONErrorBool。] ## 使用類型別名創建實例 Haskell 98標準不允許我們用下面的形式聲明實例,盡管它看起來沒什么問題: ~~~ -- 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。根據 Haskell 98的規則,我們在聲明實例的時候不能用具體類型替代類型變量。也就是說,我們可以給 [a] 聲明實例,但給 [Char] 不行。 盡管 GHC 默認遵守 Haskell 98標準,但是我們可以在文件頂部添加特殊格式的注釋來解除這個限制。 ~~~ -- file: ch06/JSONClass.hs {-# LANGUAGE TypeSynonymInstances #-} ~~~ 這條注釋是一條編譯器指令,稱為*編譯選項(pragma)*,它告訴編譯器允許這項語言擴展。上面的代碼因為``TypeSynonymInstances`` 這項語言擴展而合法。我們在本章(本書)還會碰到更多的語言擴展。 [譯注:作者舉的這個例子實際上牽涉到了兩個問題。第一,Haskell 98不允許類型別名,這個問題可以通過上述方法解決。第二,Haskell 98不允許 [Char] 這種形式的類型,這個問題需要通過增加另外一條編譯選項 {-#LANGUAGEFlexibleInstances#-} 來解決。] ## 生活在開放世界 Haskell 的設計允許我們任意創建類型類實例。 ~~~ -- 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 ~~~ 我們可以在任意地方創建新實例,而不僅限于在定義了類型類的模塊中。類型類系統的這個特性被稱為*開放世界假設*(open world assumption)。如果有方法表示“這個類型類只存在這些實例”,那我們將得到一個*封閉的*世界。 我們希望把列表轉為 JSON 數組。現在先不用關心實現細節,暫時用 undefined 替代函數內容即可。 ~~~ -- file: ch06/BrokenClass.hs instance (JSON a) => JSON [a] where toJValue = undefined fromJValue = undefined ~~~ 我們也希望能將鍵/值對列表轉為 JSON 對象。 ~~~ -- file: ch06/BrokenClass.hs instance (JSON a) => JSON [(String, a)] where toJValue = undefined fromJValue = undefined ~~~ ## 什么時候重疊實例(Overlapping instances)會出問題? 如果我們把這些定義放進文件中并在 **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 ~~~ 然而,當我們使用序對列表實例時,麻煩來了。 ~~~ *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")] ~~~ 重疊實例問題是由 Haskell 的開放世界假設造成的。 這里有一個更簡單的例子來說明發生了什么。 ~~~ -- 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 ++ "<<" ~~~ 對于序對,我們有兩個 Borked 類型類實例:一個是 Int 序對,另一個是任意類型的序對,只要這個類型是 Borked 類型類的實例。 假設我們想把 bork 應用于 Int 序對。編譯器必須選擇一個實例來用。由于這兩個實例都能用,所以看上去它好像只要選那個更相關(specific)的實例就可以了。 但是,GHC 默認是保守的。它堅持只能有一個可用實例。這樣,當我們試圖使用 bork 時,它就會報錯。 Note 重疊實例什么時候會出問題? 之前我們提到,我們可以把某個類型類的實例分散在幾個模塊中。GHC 并不會在意重疊實例的存在。相反,只有當我們使用受影響的類型類的函數,GHC 被迫要選擇使用哪個實例時,它才會報錯。 ## 取消類型類的一些限制 通常,我們不能給多態類型(polymorphic type)的特化版本(specialized version)寫類型類實例。[Char] 類型就是多態類型 [a] 特化成 Char 的結果。因此我們禁止聲明 [Char] 為某個類型類的實例。這非常不方便,因為字符串在代碼中無處不在。 FlexibleInstances 語言擴展取消了這個限制,它允許我們寫這樣的實例。 GHC 支持另外一個有用的語言擴展,OverlappingInstances,它解決了重疊實例帶來的問題。如果存在重疊實例,編譯器會選擇最相關的(specific)那一個。 我們經常把這個擴展和 TypeSynonymInstances 放在一起使用。下面是一個例子。 ~~~ -- 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 ~~~ 如果我們對 String 應用 foo,編譯器會選擇 String 的特定實現。即使 [a] 和 Char 都是 Foo 的實例,但由于 String 實例更相關,因此 GHC 選擇了它。 即使開了 OverlappingInstances 擴展,如果 GHC 發現了多個同樣相(equally specific)關的實例,它仍然會拒絕代碼。 > 何時使用 OverlappingInstances 擴展(to be added) ## 字符串的 show 是如何工作的? OverlappingInstances 和 TypeSynonymInstances 語言擴展是 GHC 特有的,Haskell 98 并不支持。然而,Haskell 98 中的 Show 類型類在轉化 Char 列表和 Int 列表時卻用了不同的方法。它用了一個聰明但簡單的小技巧。 Show 類型類定義了轉換單個值的 show 方法和轉換列表的 showList 方法。showList 默認使用中括號和逗號轉換列表。 [a] 的 Show 實例使用 showList 實現。Char 的 Show 實例提供了一個特殊的 showList 實現,它使用雙引號,并轉義非 ASCII 打印字符。 結果是,如果有人想對 [Char] 應用 show,編譯器會選擇 showList 的實現,并使用雙引號正確轉換這個字符串。 這樣,換個角度看問題,我們就能避免 OverlappingInstances 擴展了。 ## 如何給類型定義新身份(Identity) 除了熟悉的 data 關鍵字外,Haskell 還允許我們用 newtype 關鍵字來創建新類型。 ~~~ -- file: ch06/Newtype.hs data DataInt = D Int deriving (Eq, Ord, Show) newtype NewtypeInt = N Int deriving (Eq, Ord, Show) ~~~ newtype 聲明的作用是重命名現有類型,并給它一個新身份。可以看出,它的用法和使用 data 關鍵字進行類型聲明看起來很相似。 Note type 和 newtype 關鍵字 盡管名字類似,type 和 newtype 關鍵字的作用卻完全不同。type 關鍵字給了我們另一種指代某個類型的方法,類似于給朋友起的綽號。我們和編譯器都知道 [Char] 和 String 指的是同一個類型。 相反,newtype 關鍵字的存在是為了隱藏類型的本性。考慮這個 UniqueID 類型。 ~~~ -- file: ch06/Newtype.hs newtype UniqueID = UniqueID Int deriving (Eq) ~~~ 編譯器會把 UniqueID 當成和 Int 不同的類型。作為 UniqueID 的用戶,我們只知道它是一個唯一標識符;我們并不知道它是用 Int 來實現的。 在聲明 newtype 時,我們必須決定暴露被重命名類型的哪些類型類實例。這里,我們讓 NewtypeInt 提供 Int 類型的 Eq, Ord 和 Show 實例。這樣,我們就可以比較和打印 NewtypeInt 類型的值了。 ~~~ *Main> N 1 < N 2 True ~~~ 由于我們沒有暴露 Int 的 Num 或 Integral 實例,NewtypeInt 類型的值并不是數字。例如,我們不能做加法。 ~~~ *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 關鍵字一樣,我們可以用 newtype 的值構造器創建新值,或者對現有值進行模式匹配。 如果 newtype 沒用自動派生來暴露對應類型的類型類實現的話,我們可以自己寫一個新實例或者干脆不實現那個類型類。 data 和 newtype 的區別 newtype 關鍵字給現有類型一個不同的身份,相比起 data,它使用時的限制更多。具體來講,newtype 只能有一個值構造器, 并且這個構造器只能有一個字段。 ~~~ -- file: ch06/NewtypeDiff.hs -- 可以:任意數量的構造器和字段 data TwoFields = TwoFields Int Int -- 可以:一個字段 newtype Okay = ExactlyOne Int -- 可以:使用類型變量 newtype Param a b = Param (Either a b) -- 可以:使用記錄語法 newtype Record = Record { getInt :: Int } -- 不可以:沒有字段 newtype TooFew = TooFew -- 不可以:多于一個字段 newtype TooManyFields = Fields Int Int -- 不可以:多于一個構造器 newtype TooManyCtors = Bad Int | Worse Int ~~~ 除此之外,data 和 newtype 還有一個重要區別。由 data 關鍵字創建的類型在運行時有一個簿記開銷,如記錄某個值是用哪個構造器創建的。而 newtype 只有一個構造器,所以不需要這個額外開銷。這使得它在運行時更省時間和空間。 由于 newtype 的構造器只在編譯時使用,運行時甚至不存在,用 newtype 定義的類型和用 data 定義的類型在匹配 undefined 時會有不同的行為。 為了理解它們的不同點,我們首先回顧一下普通數據類型的行為。我們已經非常熟悉,在運行時對 undefined 求值會導致崩潰。 ~~~ Prelude> undefined *** Exception: Prelude.undefined ~~~ 我們把 undefined 放進 D 構造器創建一個 DataInt,然后對它進行模式匹配。 ~~~ *Main> case (D undefined) of D _ -> 1 1 ~~~ 由于我們的模式匹配只匹配構造器而不管里面的值,undefined 未被求值,因而不會拋出異常。 下面的例子沒有使用 D 構造器,因而模式匹配時 undefined 被求值,異常拋出。 ~~~ *Main> case undefined of D _ -> 1 *** Exception: Prelude.undefined ~~~ 當我們用 N 構造器創建 NewtypeInt 值時,它的行為與使用 DataInt 類型的 D 構造器相同:沒有異常。 ~~~ *Main> case (N undefined) of N _ -> 1 1 ~~~ 但當我們把表達式中的 N 去掉,并對 undefined 進行模式匹配時,關鍵的不同點來了。 ~~~ *Main> case undefined of N _ -> 1 1 ~~~ 沒有崩潰!由于運行時不存在構造器,匹配 N_ 實際上就是在匹配通配符 _:由于通配符總可以被匹配,所以表達式是不需要被求值的。 ## 命名類型的三種方式 這里簡要回顧一下 haskell 引入新類型名的三種方式。 - data 關鍵字定義一個真正的代數數據類型。 - type 關鍵字給現有類型定義別名。類型和別名可以通用。 - newtype 關鍵字給現有類型定義一個不同的身份(distinct identity)。原類型和新類型不能通用。 ## JSON typeclasses without overlapping instances ## 可怕的單一同態限定(monomorphism restriction) Haskell 98 有一個微妙的特性可能會在某些意想不到的情況下“咬”到我們。下面這個簡單的函數展示了這個問題。 ~~~ -- file: ch06/Monomorphism.hs myShow = show ~~~ 如果我們試圖把它載入 **ghci**,會產生一個奇怪的錯誤: ~~~ 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 ‘a0’ 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 得到的輸出。] 錯誤信息中提到的 “monomorphism” 是 Haskell 98 的一部分。 單一同態是多態(polymorphism)的反義詞:它表明某個表達式只有一種類型。 Haskell 有時會強制使某些聲明不像我們預想的那么多態。 我們在這里提單一同態是因為盡管它和類型類沒有直接關系,但類型類給它提供了產生的環境。 Note 在實際代碼中可能很久都不會碰到單一同態,因此我們覺得你沒必要記住這部分的細節, 只要在心里知道有這么回事就可以了,除非 GHC 真的報告了跟上面類似的錯誤。 如果真的發生了,記得在這兒曾讀過這個錯誤,然后回過頭來看就行了。 我們不會試圖去解釋單一同態限制。Haskell 社區一致同意它并不經常出現;它解釋起來很棘手(tricky); 它幾乎沒什么實際用處;它唯一的作用就是坑人。舉個例子來說明它為什么棘手:盡管上面的例子違反了這個限制, 下面的兩個編譯起來卻毫無問題。 ~~~ -- file: ch06/Monomorphism.hs myShow2 value = show value myShow3 :: (Show a) => a -> String myShow3 = show ~~~ 上面的定義表明,如果 GHC 報告單一同態限制錯誤,我們有三個簡單的方法來處理。 - 顯式聲明函數參數,而不是隱性。 - 顯式定義類型簽名,而不是依靠編譯器去推導。 - 不改代碼,編譯模塊的時候用上 NoMonomorphismRestriction 語言擴展。它取消了單一同態限制。 沒人喜歡單一同態限制,因此幾乎可以肯定的是下一個版本的 Haskell 會去掉它。但這并不是說加上 NoMonomorphismRestriction 就可以一勞永逸:有些編譯器(包括一些老版本的 GHC)識別不了這個擴展,但用另外兩種方法就可以解決問題。如果這種可移植性對你不是問題,那么請務必打開這個擴展。 ## 結論 在這章,你學到了類型類有什么用以及怎么用它們。我們討論了如何定義自己的類型類,然后又討論了一些 Haskell 庫里定義的類型類。最后,我們展示了怎么讓 Haskell 編譯器給你的類型自動派生出某些類型類實例。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看