Haskell 入門筆記(四)

weixin_34194087發表於2016-10-09

型別系統

強大的型別系統是 Haskell的 一個非常大的優勢。

Haskell 所有表示式型別在編譯時判斷。這樣的話,可以使得程式碼更加安全,比如說,拿一個整數和一個字串進行除法運算是沒辦法進行的,那麼在編譯器就會直接報錯,不會等到執行時程式崩潰才知道。Haskell 與 Java 不一樣,Haskell 能夠進行型別推斷(Type Inference),也就是說,你不需要明確的說 100 是個數字,或者說是整型,編譯時能推斷出這是一個整型。

在 GHCi 中,我們可以使用 :t 命令來檢測一個表示式的型別。

Prelude> :t 'q' 
'q' :: Char

Prelude> :t "aaa" 
"aaa" :: [Char]

:: 操作符的含義是「具有 … 型別」。也就是說,根據上面的結果,我們知道,字元 q 的型別是 「Char」。一般來說,Haskell 的型別的首字母都是大寫,比如上面提到的 Char,還有 Bool 或者 Boolean。[] 代表 List,[Char] 代表元素型別為 Char 的 List。() 則代表 Tuple,('a','a')的型別是 (Char,Char)

顯式型別宣告

除了表示式之外,函式也是有型別的。我們在定義函式的時候,可以顯式地給函式宣告其型別。我們在前面講過一個去除字串中大寫字母的 List Comprehension:

removeNonUppercase st = [c | c <- st, c `elem` ['A'..'Z']]

對於這樣一個函式,很明顯,其輸入和輸出都是字串,也就是字元的 List,因此,我們可以這樣宣告函式的型別:

removeNonUppercase :: [Char]->[Char]a

上面這個宣告的含義是,函式 removeNonUppercase 接收一個[Char] 型別的引數(例如字串),並且返回一個 [Char](例如字串)。那怎麼去指定一個接收多個引數的函式的型別呢?比如說有一個函式叫 addThree,接收三個引數,將這三個引數的值相加並且返回。我們可以這樣指定 addThree 的函式型別

addThree :: Int->Int->Int->Int

也就是說,最後一個會被當做返回值來解析,前面的都會被當做引數來解析。如果說你不知道你要寫的函式到底應該是什麼型別,你可以先把函式寫出來,然後使用 :t 命令看看到底是什麼型別,最後再補上函式型別。

常見的Haskell型別

型別 說明
Int 整型,但是能表示的整數有界限(達到一定程度就會溢位),效率更高
Integer 整型,能夠表示的整數沒有界限,效率低
Float 單精度浮點數
Double 雙精度浮點數
Bool 布林值,只有 True 和 False 兩個值
Char 單個Unicode字元
Tuple 具體的 Tuple 型別取決於元素的型別和個數,理論上有無數 Tuple 型別,但是實際上Tuple最多隻能有63個元素

型別變數(Type Variable)

有時候函式需要能夠處理多種型別的資料,我們以 head 函式為例。首先看看 head 函式的型別:

Prelude> :t head 
head :: [a] –> a

我們可以看到,函式 head 接收一個 List 作為輸入,返回 List 中的一個元素。但是這個元素到底是 Char 還是 Int 還是 Bool 並不重要。這個 a 是什麼?我們說過所有的型別都是以大寫字母打頭的,a 顯然不是一種我們所不知道的型別。a 實際上就是我們這裡說的型別變數的一個例子。型別變數能夠允許函式以一種安全的方式操作多種型別,這一點類似於 Java 中的泛型。使用型別變數的函式在 Haskell 中稱為多型函式(Polymorphyc function)。head 函式的定義的含義是:head 接收一個裝有任何元素的 List,返回這種型別一個值。英語中單詞 a 也表示泛指, a pen, a apple 等等。

我們再看看 fst 函式的型別定義:

Prelude> :t fst 
fst :: (a, b) –> a

這個函式接收一個 pair,然後返回第一個元素,至於這個 pair 的元素可以是任何型別,這裡的a,b都是型別變數。需要說明的是,這裡的 a 和 b 雖然都是型別變數,但是不意味著他們一定是不同的型別。a,b 這種型別變數就像佔位符變數一樣,表示這個地方有一個某某型別的變數。

Type Class

Type Class 我也不知道該怎麼翻譯比較合適。Type Class 實際上是一種介面,它定義一些行為,當某個變數是這個 Type Class 的例項時,那麼它可以實現這個 Type Class 所描述的行為。Type Class 一般指定一組函式,一個變數是該 Type Class 的例項,我們就需要確定這些函式對於這個變數本身有什麼意義(也就是說這個變數要有自己的實現)。

定義相等性的 Type Class 就是一個很好的例子。很多型別都可以用 == 來看值是否相等。我們先看看 == 運算子的函式簽名:

Prelude> :t (==) 
(==) :: Eq a => a -> a –> Bool

實際上 == 是一個函式,基本上 +-* 以及幾乎所有的運算子都是函式。這裡出現了一個新的符號 =>,所有出現在這個符號之前的部分叫做 class constraint(類的約束)。這個函式型別的意思是:== 函式接收兩個值,他們同樣屬於型別 Eq,函式最終返回一個 Bool 值。

Eq 就屬於 Type Class,它提供了判斷值是否相等的介面。而這些值必須是相同型別才有比較的意義,這些值可以是 Eq 的例項。事實上,在標準的 Haskell 中,幾乎所有型別都是 Eq 的例項。需要特別指出的是,Type Class 並不是物件導向程式語言中的 Class。下面我們一起看看 Haskell 中常見的幾種 Type Class:

  • Eq

Eq 用來提供檢測值是否相等的介面。它的兩個實現是 ==/=。這意味著如果在一個函式的定義中出現了 Eq class constraint,那麼這個函式的定義中肯定用到了 == 或者是 /=。如果一種型別實現一個函式,他就要定義使用這個型別的值時,該函式到底做些什麼。我們看幾個 Eq 例項進行相等性比較時的例子:

Prelude> 5 == 5 
True 
Prelude> 'q' == 'q' 
True 
Prelude> "Hello"=="hello" 
False 
Prelude> "Hello"=="Hello" 
True 
Prelude> pi == 3.14 
False

我們可以看到,字串的比較規則是遵循 List 的相等性比較,與 Java 中的比較引用是不一樣的。

  • Ord

Ord 是一種為那些可以將值放在某種順序排列中的型別設計的 Type Class。我們看看 > 函式的型別:

Prelude> :t (>) 
(>) :: Ord a => a -> a –> Bool

>== 比較類似,都接收兩個引數,然後返回一個 Bool 值。Ord Type Class 涉及到了所有的比較函式:><>=<=

compare 函式接收兩個引數,這兩個引數的型別都是 Ord 的例項,然後返回一個 Ordering。Ordering 是一個值可以是 GT、LT 或者 EQ 的型別,分別代表大於、小於和等於。我們看幾個例子:

Prelude> "abcd" `compare` "bbcd" 
LT 
Prelude> "abcd" `compare` "abbd" 
GT 
Prelude> "abcd" `compare` "abcd" 
EQ
  • Show

型別是 Show 這個 Type Class 的例項的值可以被顯示為字串。對於所有屬於 Show 這個 Type Class 的例項的型別來說,使用最多的函式式 show(s小寫)。我們看幾個例子:

Prelude> show 3 
"3" 
Prelude> show True 
"True"
  • Read

Read 可以看做是 Show 的反面。read 函式接收一個字串,然後返回一個型別是 Read 的例項的值。看例子:

Prelude> read "True" || False 
True

Prelude> read "5"-2 
3

Prelude> read "[1,2,3,4]" ++ [5] 
[1,2,3,4,5]

目前為止都一切正常,我們再看一個例子:

Prelude> read "5"

<interactive>:30: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 "5" 
    In an equation for `it': it = read "5"

當我們直接 read "5" 時,GHCi 不知道該返回什麼。我們之前的例子都將 read 返回的結果再參與某種運算,這樣 GHCi 才好進行型別推斷,這就是為什麼 read "5" 沒辦法返回值的原因。我們看一下read 函式的型別:

Prelude> :t read 
read :: Read a => String –> a

我們看到,read 函式接收 String,但是返回一個型別是 Read 的例項的值。但是型別是 Read 例項的型別太多了,GHCi 不知道到底選哪一種型別。這種情況下,我們可以使用型別註解(type annotation)。我們看例子是最直接的:

Prelude> read "5" :: Int 
5 
Prelude> read "5" :: Float 
5.0

對於 read 來說還需要舉一個例子:

Prelude> [read "True",False,True,False] 
[True,False,True,False]

因為 List 中的每一個元素必須屬於同種型別,所以 read "True" 的返回值必須和其他元素型別一樣,也就是 Bool,這樣,GHCi 就知道該怎麼返回值了。

  • Enum

Enum 的例項是那種值有序的型別——他們的值可以被列舉。Enum Type Class 最大的優勢是可以在 Ranges 中使用其值。他們還定義了successors 和 predecessors, 我們可以分別通過 succ 和 pred 兩個函式獲得。Bool、Char、Ordering、Int、Integer、Float、Double 是這個 Type Class 的例項,我們看例子:

Prelude> ['a'..'e'] 
"abcde"

Prelude> [LT .. GT] 
[LT,EQ,GT] 
Prelude> [3 .. 5] 
[3,4,5] 
Prelude> succ 'B' 
'C' 
Prelude> pred 'B' 
'A'
  • Bounded

那些是 Bounded 例項的型別有一個上限值和一個下限值。分別可以使用 minBound 和 maxBound 檢視:

Prelude> minBound::Int 
-2147483648 
Prelude> maxBound::Int 
2147483647

minBound 和 maxBound 的型別都是 Bounded a=>a。準確來說,他們是多型常量。Tuple 中所有元素型別都是 Bounded 的話,那麼這個 Tuple 也被認為是 Bounded 的例項。

  • Num

Num 是數字 Type Class,它的例項都是數字。所有的數字都是多型常量。也就是說我們可以將它制定成 Num 下屬型別中的任何一種:

Prelude> 6::Int 
6 
Prelude> 6::Float 
6.0

要成為 Num Type Class 的例項,這個型別必須要已經是 Eq 和 Show Type Class 的例項。

  • Floating

顧名思義,這種 Type Class 的例項型別就是用來儲存浮點數的,就兩種型別 Float 和 Double。

  • Integral

包括 Int 和 Integer 兩種。介紹兩個函式 fromIntegral 和 length,先看看兩個函式的簽名,再看看怎麼使用:

Prelude> :t fromIntegral 
fromIntegral :: (Integral a, Num b) => a -> b 
Prelude> :t length 
length :: [a] –> Int

Prelude> fromIntegral (length [1,2,3,4]) + 3.4 
7.4

Tips

Type Class 實際上是一個抽象的介面,所以一個型別可以是多種 Type Class 的例項,同樣,一種 Type Class 有很多例項;
有時候一種型別必須先是一種 Type Class 的例項才會被允許成為另一個Type Class 的例項。

相關文章