Haskell/型別基礎 II
在本章中,我們將展示 Haskell 如何處理數值型別,並介紹型別系統的一些重要特性。 不過,在深入文字之前,請暫停片刻,思考以下問題:(+) 函式的型別應該是什麼?[1]
數學對我們可以加在一起的數字型別幾乎沒有限制。 例如,考慮 (兩個自然數)、(一個負整數和一個有理數)或 (一個有理數和一個無理數)。 所有這些都是有效的。 事實上,任何兩個實數都可以加在一起。 為了以最簡單的方式儘可能地捕捉這種通用性,我們需要在 Haskell 中使用通用的 Number 型別,這樣 (+) 的簽名就簡單地為
(+) :: Number -> Number -> Number
然而,這種設計與計算機執行算術的方式很不相符。 雖然計算機可以將整數作為記憶體中的一系列二進位制數字來處理,但這種方法不適用於實數,[2] 因此需要更復雜的編碼來處理它們:浮點數。 雖然浮點數提供了一種處理一般實數的合理方法,但它也有一些不便之處(最值得注意的是精度損失),這使得使用更簡單的編碼處理整數值變得更有價值。 所以,我們至少有兩種不同的儲存數字的方法:一種用於整數,另一種用於一般實數。 每個方法都應該對應不同的 Haskell 型別。 此外,計算機只有在兩個數字格式相同的情況下才能執行像 (+) 這樣的運算。
所以,擁有一個通用的 Number 型別就更不用說了——似乎我們甚至不能讓 (+) 混合整數和浮點數。 然而,Haskell 可以至少使用相同的 (+) 函式來處理整數或浮點數。 在 GHCi 中自己驗證一下
Prelude> 3 + 4 7 Prelude> 4.34 + 3.12 7.46
在討論列表和元組時,我們看到,如果函式是多型的,它們可以接受不同型別的引數。 本著這種精神,這裡有一個可能的 (+) 型別簽名,它將說明上述事實
(+) :: a -> a -> a
有了這個型別簽名,(+) 將接受兩個相同型別 a 的引數(可以是整數或浮點數),並計算出一個型別為 a 的結果(只要兩個引數型別相同即可)。 但是,這個型別簽名表示任何型別,我們知道我們不能對兩個 Bool 值或兩個 Char 值使用 (+)。 新增兩個字母或兩個真值是什麼意思? 所以,(+) 的實際型別簽名使用了一種語言特性,允許我們表達語義限制,即 a 可以是任何型別,只要它是數字型別
(+) :: (Num a) => a -> a -> a
Num 是一個型別類——一組型別——它包含所有被視為數字的型別。 [3] 簽名中的 (Num a) => 部分將 a 限制為數字型別——或者,用 Haskell 的術語來說,是 Num 的例項。
那麼,實際的數字型別有哪些(也就是說,簽名中的 a 可以代表的 Num 例項)? 最重要的數值型別是 Int、Integer 和 Double
Int對應於大多數語言中常見的普通整數型別。 它具有固定的最大值和最小值,這些值取決於計算機的處理器。 (在 32 位機器中,範圍從 -2147483648 到 2147483647)。
Integer也用於整數,但它支援任意大的值——以犧牲一些效率為代價。
Double是雙精度浮點數型別,在絕大多數情況下,它是實數的良好選擇。 (Haskell 還有Float,它是Double的單精度對應物,通常由於精度進一步損失而不太有吸引力。)
還有其他幾種數值型別可用,但這些型別涵蓋了日常任務中的大多數數值型別。
如果你已經仔細閱讀了到目前為止的內容,你就知道我們不需要總是指定型別,因為編譯器可以推斷型別。 你還知道,當函式需要匹配型別時,我們不能混合型別。 將此與我們對數字的新理解結合起來,瞭解 Haskell 如何處理像這樣的基本算術
Prelude> (-7) + 5.12 -1.88
這似乎添加了兩種不同型別的數字——一個整數和一個非整數。 讓我們看看我們輸入的數字的實際型別是什麼
Prelude> :t (-7) (-7) :: (Num a) => a
所以,(-7) 不是 Int 也不是 Integer! 相反,它是一個多型的值,可以“變形”為任何數字型別。 現在,讓我們看一下另一個數字
Prelude> :t 5.12 5.12 :: (Fractional t) => t
5.12 也是一個多型的值,但它是 Fractional 類的值,Fractional 類是 Num 的子集(每個 Fractional 都是 Num,但並非每個 Num 都是 Fractional;例如,Int 和 Integer 不是 Fractional)。
當 Haskell 程式計算 (-7) + 5.12 時,它必須為數字確定一個實際的匹配型別。 型別推斷考慮了類規範:(-7) 可以是任何 Num,但 5.12 有額外的限制,所以它是限制因素。 在沒有其他限制的情況下,5.12 將假定 Double 的預設 Fractional 型別,因此 (-7) 也將成為 Double。 然後,加法運算將正常進行,並返回一個 Double。 [4]
以下測試將讓你更好地瞭解這個過程。 在一個原始檔中,定義
x = 2
然後在 GHCi 中載入檔案並檢查 x 的型別。 然後,將檔案更改為新增一個 y 變數,
x = 2
y = x + 3
重新載入它並檢查 x 和 y 的型別。 最後,將 y 修改為
x = 2
y = x + 3.1
看看兩個變數的型別會發生什麼。
數值型別和類的複雜性有時會導致一些問題。例如,考慮常見的除法運算子(/)。它具有以下型別簽名
(/) :: (Fractional a) => a -> a -> a
將a限制為分數型別是必須的,因為兩個整數的除法通常會導致非整數。然而,我們仍然可以寫類似的東西
Prelude> 4 / 3 1.3333333333333333
因為字面量4和3是多型值,因此在(/)的支配下,它們假設型別Double。然而,假設我們想要將一個數字除以列表中的元素數量。[5] 很明顯應該使用length函式,它接受一個列表並返回其中的元素數量
Prelude> length [1,2,3] 3 Prelude> 4 / length [1,2,3]
不幸的是,它會爆炸
<interactive>:1:0:
No instance for (Fractional Int)
arising from a use of `/' at <interactive>:1:0-17
Possible fix: add an instance declaration for (Fractional Int)
In the expression: 4 / length [1, 2, 3]
In the definition of `it': it = 4 / length [1, 2, 3]
像往常一樣,可以透過檢視length的型別簽名來理解問題
length :: (Foldable t) => t a -> Int
現在,讓我們關注length結果的型別。它是Int;結果不是多型的。由於Int不是Fractional,Haskell不會讓我們將其與(/)一起使用。
為了解決這個問題,我們有一個特殊的函式。在繼續文字之前,嘗試根據名稱和簽名猜測它的功能
fromIntegral :: (Integral a, Num b) => a -> b
fromIntegral接受某個Integral型別的引數(如Int或Integer),並將其轉換為多型值。透過將其與length結合,我們可以使列表的長度符合(/)的簽名
Prelude> 4 / fromIntegral (length [1,2,3]) 1.3333333333333333
在某些方面,這個問題很煩人且乏味,但它是嚴格處理數字的必然結果。在Haskell中,如果你定義了一個帶有Int引數的函式,它永遠不會轉換為Integer或Double,除非你明確使用fromIntegral之類的函式。作為其完善的型別系統的直接結果,Haskell 在處理數字方面具有令人驚訝的多樣化類別和函式。
Haskell 擁有超越算術的型別類。例如,(==)的型別簽名是
(==) :: (Eq a) => a -> a -> Bool
與(+)或(/)類似,(==)是一個多型函式。它比較兩個相同型別的的值,這些值必須屬於Eq類,並返回一個Bool。Eq只是用於可以比較相等性的值的型別的類,它包含所有基本非函式型別。 [6]
我們已經忽略的一個完全不同的型別類示例出現在length的型別中。鑑於length接受一個列表並返回一個Int,我們可能期望它的型別為
length :: [a] -> Int
然而,實際型別有點複雜
length :: (Foldable t) => t a -> Int
除了列表之外,還有其他型別的結構可以用來以不同的方式對值進行分組。許多這些結構(以及列表本身)屬於一個稱為Foldable的型別類。length的型別簽名告訴我們,它不僅適用於列表,也適用於所有其他Foldable結構。在本書的後面,我們將看到這種結構的示例,並詳細討論Foldable。在此之前,每當你在型別簽名中看到類似Foldable t => t a的內容時,可以隨意將其在腦海中替換為[a]。
型別類為型別系統增添了巨大的力量。我們將在後面回到這個主題,看看如何在自定義方式中使用它們。
注意
- ↑ 如果你按照我們在“型別基礎”中的建議,你可能已經透過使用
:t測試看到了相當奇特的答案...... 如果是這樣,請將以下分析視為理解該簽名的含義的途徑。 - ↑ 除了其他問題,在任何兩個實數之間,有不可數的實數——無論我們做什麼,這個事實都無法直接對映到記憶體中的表示形式。
- ↑ 這是一個寬鬆的定義,但在我們更詳細地討論型別類之前足夠了。
- ↑ 對於經驗豐富的程式設計師:這似乎具有與 C(以及許多其他語言)中的程式使用隱式轉換(其中整數字面量被靜默轉換為雙精度浮點數)相同的效果。然而,在 C 中,轉換是在你的背後完成的,而在 Haskell 中,只有當變數/字面量是多型值時才會發生。這個區別很快就會變得更清楚,當我們展示一個反例時。
- ↑ 一個合理的場景——想想計算列表中值的平均值。
- ↑ 比較兩個函式的相等性被認為是棘手的