Haskell Tutorial(2)一絲不苟的型態系統 by caterpillar | CodeData
top

Haskell Tutorial(2)一絲不苟的型態系統

分享:

Haskell Tutorial(1)哈囉!世界! << 前情

我在〈靜態定型與單元測試之爭〉談過「任何數值都是記憶體中的一組位元,型態賦予這組位元意義,這樣開發者就能得知如何對待這組位元,因此型態也部份解釋了開發者想要程式做哪些事情。」

也因此,任何程式語言的學習,都得認識型態系統,就結論而言,Haskell 是靜態定型(Static type)強型別(Strong type)並具有強大型態推斷(Type inference)能力的語言。

靜態定型

靜態定型表示編譯器在編譯時期,就可以得知各個值與運算式(expression)的型態,舉例來說,你可以設計一個簡單的函式(Function):

doubleMe :: Float -> Float
doubleMe x = x + x

雖然還沒有正式要介紹函式,不過,這個函式很簡單,第一行是函式的型態宣告,函式名稱是 doubleMeFloat -> Float 表示接受 Float 引數並傳回 Float 結果,x 是接受引數的參數名稱,函式的傳回值是 x + x 運算式的結果。

你可以將之儲存為 myfuncs.hs,然後在 GHCI 中使用 :l 來載入:

靜態定型

可以看到,doubleMe 3.14 表示使用 3.14 來呼叫函式 doubleMe,這沒問題,因為 3.14 被推斷為一個 Float,然而 doubleMe "3.14" 就不行,因為 "3.14" 不會是一個 Float,靜態定型的 Haskell 會在執行之前,就檢查出這類型態不匹配的錯誤,避免許多執行時期因型態不正確而可能引發的錯誤。

強型別

注意!上面我說 doubleMe 3.14 時,3.14 被推斷為一個 Float,因為上面 3.14 這 literal 本身沒有指定型態,因而編譯器試圖為它推斷一個適合的型態。你可以自行指定型態。例如:

指定型態

當你指定 3.14::Float 時,表示 3.14 的型態就是 Float,可以看到,當你指定 1::Int1::Double並試圖呼叫 doubleMe 時,就會發生編譯錯誤,因為函式只接受 Float 的引數。

就多數主流的靜態語言來說,不能將 double 之類的值指定給 float 比較容易理解,因為可以解釋為記憶體長度不同(必要時可以使用 CAST 語法來關閉編譯器檢查),然而 1::Int 不能指定給 Float 的參數,就比較覺得令人詫異了,在多數主流的靜態語言中,將 int 指定給 float 之類的變數是允許的。

在 Haskell 中,1::Int 不能指定給 Float 的參數說明了,Haskell 是座落於強型別這側的語言,強型別意謂著,型態轉換不會自動發生,如果函式預期接受 Float,而你給他一個 Int 引數,引數並不會自動轉換為 Float

Haskell 的型態系統有多嚴格?來看看 Int 與浮點數相加會如何?

Int 與浮點數相加

在 Haskell 中,會不意外地發生編譯錯誤,這類錯誤當然不會像這邊範例這麼直接發生,而會像是以下這種情況:

Int 與浮點數相加

[1, 2, 3] 在 Haskell 中會建立一個清單, length 函式可以取得這個清單的長度,以 Int 傳回,Int 與 3.14 相加就會引發錯誤。類似地,以下也會發生錯誤:

整數與浮點數相加

型態推斷

看到以上的範例,你可能會有疑問,那麼 10 + 3.14 為什麼可以?

10 + 3.14

如前所述,這是因為編譯器推斷出這兩個 literal(嚴格來說,是推斷出 (10 + 3.14) 這個運算式)最適合的型態 Fractional,在這邊,:t 是可以在 GHCI 中用來檢驗型態的指令,你可以隨時用它來檢驗 Haskell 中任何值的型態。

然後,當你令某代數 x 與 y 為某值時,因為前後程式文脈(Context)的沒有型態可供參考(像是 + 函式這類),編譯器會分別給予的型態會像是:

令某代數 x 與 y 為某值

這也就是為何,x + y 不能通過編譯的原因,如果你想要令其通過編譯,可以使用 fromInteger 函式,例如:

fromInteger

可以看到,fromInteger 的型態宣告是 Num a => Integer -> aNum 是個 Typeclass,某些程度上,你可以將 Typeclass 類比為 Java 中的 interfaceNum a 表示 a 必須是個具有 Num 行為的型態,Integer -> a 表示參數型態為 Integer,而傳回型態為 a

所有的整數與浮點數都有 Num 的行為,當你執行 fromInteger x + y 時,編譯器會根據 + y 決定,fromInteger x 的傳回值型態會是 Double,接著再與 y 進行相加。

不過,如果你是這麼寫,那麼 fromInteger 就吐了:

fromIntegral

記得嗎?fromInteger 的型態宣告是 Num a => Integer -> a,而在上面,你的 10 是個 Int,型態轉換在 Haskell 中不會自動發生,就算是 Int 自動轉為 Integer 也不行。

fromIntegral(注意!字尾是 ral 不是 er)的型態是 (Integral a, Num b) => a -> b,表示 a 必須有 Integral 的行為,而 b 必須有 Num 的行為,Integral 也是個 Typeclass,所有整數都有 Integral 的行為,因此就涵蓋了 Int。就整個 fromIntegral (10::Int) + 3.14 程式文脈,編譯器最後推斷出的型態會是 Fractional,實際上,Fractional 也是個 Typeclass。

實際上,Haskell 的編譯器很強大,如果你沒有指定型態,總是會努力為你推斷出適合的型態,例如,先前的 doubleMe 可以只寫為:

doubleMe x = x + x

重新在 GHCI 中用 :l 載入,使用 :t 檢驗看看,編譯器為你推斷為何種型態?

型態推斷

在 Haskell 中,絕大多數的情況下,不聲明型態是可以的,這使得 Haskell 程式碼看起來,會像是動態語言中的變數無需宣告型態(實際上,Haskell 沒有變數,之後會詳述),不過,在 Haskell 中,宣告函式時明確聲明型態,反而是鼓勵的,如果你真的不知道你的函式型態要如何宣告型態,可以像這邊,在 GHCI 中檢驗完之後,再將型態宣告添加至原始碼之中。

基本型態與 Typeclass

暈頭了嗎?也許在 Haskell 中,使用函數式風格並不是最難的,使用正確型態通過編譯才是最難的,因為嚴格的強型別、靜態型態,使得在 Haskell 中要通過編譯本身就是件難事,因此有「It Compiles! Let’s ship it!」的笑話!

然而換取而來的代價是,不少因型態不正確的錯誤,在通過編譯之前都被抓出來了,很多時候確實是如此,我以為自己已經謹慎思考過型態了,編譯器卻總會抓出我沒想到的部份。

最後,來看看這篇文章中提到的幾個基本型態:

  • Int有界整數,如果是 32 位元機器,上下界會分別是 2147483647-2147483648
  • Integer無界整數,效率比較慢,不過可以儲存大整數,例如 2147483648::Int 結果會是 -21474836482147483648::Integer 才會是 2147483648。
  • FloatDouble分別代表單精度浮點數與倍精度浮點數。
  • BoolTrueFalse 兩個布林值的型態。
  • Char字元型態,之後還會看到,像 “Justin" 這個字串,其實是由字元組成的清單。

來看看這篇文章中提到的幾個 Typeclass:

  • Integral代表所有整數的 Typeclass,IntInteger 具有 Integral 的行為。
  • Floating代表所有浮點數的 Typeclass,FloatDouble 具有 Floating 的行為。
  • Fractional代表分數的 Typeclass,涵蓋了 FloatDouble
  • Num代表所有數字的 Typeclass。

當然,還有其他的,不過,認識它們全部並不是重點,真正的重點在於,如果你是從其他弱型別、動態定型,或者是型態系統上要求較寬鬆的語言,進入到 Haskell,必須得重新思考一下,型態對你而言到底是什麼意義?我曾經在 Ruby Conference Taiwan 2014 的〈Understanding Typing. Understanding Ruby.〉做過一些探討。

當你進入到 Haskell 之中,你會發現一件事「開發者對型態的思考總是不足的」,在這篇中認真地重新思考一下型態,之後繼續在 Haskell 中繼續前進時,才不至於處處碰壁。

後續 >> Haskell Tutorial(3)初探代數與函式

分享:
按讚!加入 CodeData Facebook 粉絲群

相關文章

留言

留言請先。還沒帳號註冊也可以使用FacebookGoogle+登錄留言

Jeff Chen11/10

介紹 type 的好文章:)
thanks for writing series of nice articles

some suggestions:
在示範裏 :
let x = 10
let y = 3.14
x + y
這個範例可能在正常的 ghci 裡會回傳 13.14
(since a clean prelude will infer x as Num a, not Integer)
簡而言之 Haskell 會盡可能的 infer 最寬鬆的 type 限制

第二
對於剛開始接觸的使用者可以思考看看為何 doubleMe 去除掉形態宣告後會變成 Num a => a -> a
(加法符號本身也是一個 function )
再來 加法可以用在哪些東西上? 又有哪些東西不能加?
如果很多不同形態的東西可以加在一起 那加法在不同 type 的表現是否會不同?
(function overloading + typeclasses relation)

熱門論壇文章

熱門技術文章