《Haskell趣學指南》筆記之自定義型別
系列文章
- 《Haskell趣學指南》筆記之基本語法
- 《Haskell趣學指南》筆記之型別(type)
- 《Haskell趣學指南》筆記之函式
- 《Haskell趣學指南》筆記之高階函式
- 《Haskell趣學指南》筆記之模組
- 《Haskell趣學指南》筆記之自定義型別
自定義資料型別
一、用 data 關鍵字
data 型別名 = 值構造器 | 值構造器 data Bool = False | True data Shape = Circle Float Float Float | Rectangle Float Float Float Float 複製程式碼
值構造器可以直接是一個值,如 True / False 值構造器也可以是一個名字後面加一些型別
值構造器本質上是一個返回某資料型別值的函式,所以 Circle 和 Rectangle 不是型別,是函式:
ghci> :t Circle Circle :: Float -> Float -> Float -> Shape ghci> :t Rectangle Rectangle :: Float -> Float -> Float -> Float -> Shape 複製程式碼
然後就可以使用這個型別了
area :: Shape -> Float -- 注意下面的模式匹配 area (Circle _ _ r) = pi * r ^ 2 area (Rectangle x1 y1 x2 y2) = (abs $ x2 - x1) * (abs $ y2 - y1) -- 注意下面的 Circle 和 Reactangle 的位置 ghci> area $ Circle 10 20 10 314.15927 ghci> area $ Rectangle 0 0 100 100 10000. 0 複製程式碼
但是現在如果你在 ghci 裡輸入 Circle 1 1 5 會報錯,因為 Shape 不是 Show 型別類的例項,不能被 show 函式呼叫。
解決辦法是在 data Shape 那句話的後面加一句deriving (Show)
data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving (Show) 複製程式碼
改進
Circle 函式接受三個 Float 引數,這三個引數前面兩個是圓心的座標,最後一個是半徑。
我們用 Point 型別來優化 Shape,使得它更已讀:
data Point = Point Float Float deriving (Show) -- 注意左邊的 Point 是型別名,右邊的 Point 是值構造器名(類似與建構函式麼?) data Shape = Circle Point Float | Rectangle Point Point deriving (Show) area :: Shape -> Float area (Circle _ r) = pi * r ^ 2 -- 注意下面的模式匹配 area (Rectangle (Point x1 y1) (Point x2 y2)) = (abs $ x2 - x1) * (abs $ y2 - y1) ghci> area (Rectangle (Point 0 0) (Point 100 100)) 10000. 0 ghci> area (Circle (Point 0 0) 24) 1809. 5574 複製程式碼
匯出
module Shapes ( Point(..) , -- 看這裡 Shape(..) , -- 看這裡 area , ) where 複製程式碼
其中 Shape(..) 的意思是匯出 Shape 以及 Shape 所有的值構造器,也可以寫成Shape(Circle, Rectangle)
。
當然也可以不寫括號這一部分,這樣別人就不能使用 Circle 和 Rectangle 函數了。
二、用 data + 記錄語法 record syntax
data Person = Person { firstName::String, age::Int, height::Float, phoneNumber::String, flavor::String } deriving (Show) 複製程式碼
這種語法會自動建立 firstName 等函式、允許按欄位取值。
ghci> :t firstName firstName :: Person -> String 複製程式碼
型別引數(很像泛型)
data Maybe a = Nothing | Just a 複製程式碼
Maybe 是一個型別構造器(不是型別),a 是型別引數,a 可以是 Int / Char / ...,而 Just 是個函式。
由於 Haskell 支援型別推導,所以我們只用寫 Just 'a',Haskell 就知道這是一個 Maybe Char 型別。
其實列表 [] 就是一個型別構造器,[Int] 存在,但是不存在型別 []。
Maybe 型別的使用示例:
ghci> Just "Haha" Just "Haha" ghci> :t Just "Haha" Just "Haha" :: Maybe [Char] ghci> :t Just 84 Just 84 :: (Num t) => Maybe t ghci> :t Nothing Nothing :: Maybe a ghci> Just 10 :: Maybe Double Just 10. 0 複製程式碼
data 支援類約束,但是永遠不要用
data (Ord k) => Map k v = ... 複製程式碼
書上說這隻會徒增無謂的程式碼。
如何讓一個 type 成為型別類的例項
只需要在 data 語句後面加上 deriving (Eq) 即可。
在一個型別派生為Eq的例項後,就可以直接使用==或/=來判斷它們的值的相等性了。 Haskell會先檢查兩個值的值構造器是否一致(這裡只有單值構造器),再用==來檢查其中的每一對欄位的資料是否相等。 唯一的要求是:其中所有欄位的型別都必須屬於Eq型別類。
加上 deriving (Eq, Show, Read) 就可以成為三者的例項。
Enum 型別類
data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday -- 或者加上 typeclass data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq, Ord, Show, Read, Bounded, Enum) -- 綜合目前所學 ghci> Wednesday Wednesday ghci> show Wednesday "Wednesday" ghci> read "Saturday" :: Day Saturday ghci> Saturday == Sunday False ghci> Saturday == Saturday True ghci> Saturday > Friday True ghci> Monday ` compare` Wednesday LT ghci> minBound :: Day Monday ghci> maxBound :: Day Sunday 複製程式碼
類型別名
type String = [Char] -- 注意不是 data 是 type -- 支援引數 type IntMap v = Map Int v -- 等價於 point-free 風格的下面程式碼 type IntMap = Map Int 複製程式碼
Either a b 型別
data Either a b = Left a | Right b deriving (Eq, Ord, Read, Show) 複製程式碼
書上例子挺好懂,大概意思是錯了就返回 Left "error message",對了就返回 Right "data message"。 不過我還不明白我怎麼知道 Right "data message" 是 Right 構造出來的呢?
遞迴資料結構
data List a = Empty | Cons a (List a) deriving (Show, Read, Eq, Ord) ghci> Empty Empty ghci> 5 ` Cons` Empty Cons 5 Empty ghci> 4 ` Cons` (5 ` Cons` Empty) Cons 4 (Cons 5 Empty) ghci> 3 ` Cons` (4 ` Cons` (5 ` Cons` Empty)) Cons 3 (Cons 4 (Cons 5 Empty)) 複製程式碼
自制一個列表
infixr 5 :-: data List a = Empty | a :-: (List a) deriving (Show, Read, Eq, Ord) ghci> 3 :-: 4 :-: 5 :-: Empty 3 :-: (4 :-: (5 :-: Empty)) ghci> let a = 3 :-: 4 :-: 5 :-: Empty ghci> 100 :-: a 100 :-: (3 :-: (4 :-: (5 :-: Empty))) infixr 5 ^++ (^++) :: List a -> List a -> List a Empty ^++ ys = ys (x :-: xs) ^++ ys = x :-: (xs ^++ ys) ghci> let a = 3 :-: 4 :-: 5 :-: Empty ghci> let b = 6 :-: 7 :-: Empty ghci> a ^++ b 3 :-: (4 :-: (5 :-: (6 :-: (7 :-: Empty)))) 複製程式碼
從這個例子我大概理解黃玄說的『函式式就是 symbolism』
這一年裡一直在不斷重新整理自己對「FP 是什麼」這個問題的回答… 之前覺得說「靠近/源自數學或者邏輯」吧,難道命令式/OO 的語言就不是描述數學和邏輯? 這種解釋本身不明白這個差別的人大概聽了也還是不會明白…… 今天突然覺得「(儘可能的)symbolism(符號主義)」也是個不錯的描述,從 FP 語言的歷史來看,主要的兩個祖宗 Lisp 和 ML(LCF)都起家於符號主義 AI。 即使程式語言都是符號化的,但相比於寄託於各類外接的作用,越是「FP」越是 儘可能得希望程式的行為是可以從符號中詳盡的,這因此帶來了大家說的「宣告式」、「可預測性」和「確定性」。
自制一棵二叉搜尋樹
data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show) -- 下面這個函式用來建立節點 singleton :: a -> Tree a singleton x = Node x EmptyTree EmptyTree -- 下面這個函式用來插入節點 treeInsert :: (Ord a) => a -> Tree a -> Tree a treeInsert x EmptyTree = singleton x treeInsert x (Node a left right) | x == a = Node x left right | x < a = Node a (treeInsert x left) right | x > a = Node a left (treeInsert x right) -- 下面這個函式用來判斷元素是否在樹中 treeElem :: (Ord a) => a -> Tree a -> Bool treeElem x EmptyTree = False treeElem x (Node a left right) | x == a = True | x < a = treeElem x left | x > a = treeElem x right -- 使用 ghci> let nums = [8, 6, 4, 1, 7, 3, 5] ghci> let numsTree = foldr treeInsert EmptyTree nums ghci> numsTree Node 5 (Node 3 (Node 1 EmptyTree EmptyTree) (Node 4 EmptyTree EmptyTree) ) (Node 7 (Node 6 EmptyTree EmptyTree) (Node 8 EmptyTree EmptyTree) ) 複製程式碼