我們除了裡用 product 或者 sum 來組成的我們 type 以外,還可以使用 recursive 的概念來定義 type,簡單來說就是一個 type 的定義包含他自己本身。
從一個我們最常見的 List 來看,還記得 [1,2,3,4,5] 其實只是 1:2:3:4:5:[] 的語法糖,所以其實我們可以說 List 是一個要馬是空的不然就是有一個值的 type。
data List = Empty | Cons Int List deriving(Show)
那我們使用 Cons 這個 value constructor 時大概會像是這樣
lt1 = Cons 1 Empty
:t lt -- lt :: List
lt2 = Cons 2 lt
lt2 -- Cons 2 ( Cons 1 Empty)
:t lt2 -- lt2 :: List
lt3 = 3 `Cons` lt2
lt3 -- Cons 3 (Cons 2 (Cons 1 Empty))
:t lt3 -- lt3 :: List
lt' = 1 `Cons` Empty
lt' -- Cons 1 Empty
:t lt' -- lt' :: List
會發現大致上就跟 list 的 : 很像,還記得 [1,2,3] 跟 1:2:3:[] 是同一回事,就如同我們上面先從 Cons 1 Empty 開始串接。
而且我們一樣也可以用 infix 的呼叫方式 1 Cons Empty 讓這個我們重新實作的 List 更趨近於原生的 list 。
再更進階點我們能指定特殊字元來當作我們的 value constructor
infixr 5 :!
data List' = Empty' | Int :! List' deriving (Show)
這裡會看到新的語法 fixity 宣告 infixr ,當我們需要將 function 定義成 operator 且需要改變優先權的時候就會使用到它,而 fixity 的宣告有三種 infixl、infix、infixr 分別代表左結合、無結合、右結合,而旁邊的數字 5 則是代表優先權。
就像原本的 * 、+ 這些 operator 一樣,他們也有優先權之分,像是我們都知道 * 比 + 更優先所以 3 * 3 + 3 就是等同於 (3 * 3) + 3 。
那我們也能用 :info 來看一下我們常見的 operator 的資訊
:info :
type [] :: * -> *
data [] a = ... | a : [a]
-- Defined in ‘GHC.Types’
infixr 5 :
:info +
type Num :: * -> Constraint
class Num a where
(+) :: a -> a -> a
...
-- Defined in ‘GHC.Num’
infixl 6 +
這邊告訴我們大致告訴我們 : 及 + 怎麼使用、優先級、適用的 type 。
回到我們的 List' ,我們已經用了 :! 來當作我們的 value constructor 所以程式碼就會變成
lt1' = 1 :! Empty'
lt2' = 2 :! lt1'
lt3' = 3 :! lt2'
lt1' -- 1 :! Empty'
lt2' -- 2 :! (1 :! Empty')
lt3' -- 3 :! (2 :! (1 :! Empty'))
會看到我們原本的 Cons 都已經變成 :! 看起來舒服許多。
那既然 + 、* 這些 operator 也是 function ,而且現在我們也知道我們自己定義 operator ,那我們是不是可以自己創造來類似 ++ 的operator 串接 List' ?
沒錯當然可以,而且在 Haskell 實作起來也相當簡單
infixr 5 .++
(.++) :: List' -> List' -> List'
Empty' .++ ys = ys
(x :! xs) .++ ys = x :! (xs .++ ys)
我們就把他設計的跟 ++ 很像,首先看到我們定義 .++ 的 fixity 為 infixr 5 ,以及 (.++) 這個 function 的 type 是 (.++) :: List' -> List' -> List' ,也就是他接受兩個 List' 並最後回傳一個 List' 。
再來就是pattern matching 的部分,首先我們說如果Empty' 與 ys 進行 .++ 也就是會回傳 ys
(x :! xs) .++ ys = x :! (xs .++ ys) ,這邊就是運用到遞迴的概念,如果匹配到 x:! xs 及 ys 的形式時,就是將 x 使用 :! value constructor 塞進 xs .++ ys 然後不斷遞迴直到最後一個與 Empty' 進行 .++ 運算
lt1' .++ lt2' -- 1 :! (2 :! (1 :! Empty'))
這幾天大致上把 ADT 比較重要的概念都講到了那對我來說 ADT 帶給我最大的啟發大概是用另外一種角度看 type 到底是什麼東西,它是可以組合、遞迴的而不單單只是一個描述 valuable 的語法。
在我學習 ADT 之前我一直覺得這是一個非常難以親近的概念,但實際用起來其實跟我在使用 typescript 時的概念是部分重疊,當然 typescript 的型別機制無法跟 Haskell 相比,但至少我們可以知道這些概念是可以放到其他程式語言是部分共通。
比如說 sum type 跟 ts 的 union type 好了,先有一個前提「他們兩個絕對不等價」
簡單來說的話就是,一個是 union 一個是 disjoint union
但就我們在使用的思路上會有一點雷同,通常都是我們在限制一個 valuable 只有某幾種 type 時會用到,但 Haskell 因為有 pattern matching 所以使用起來更舒服,而在 ts 大部分就是寫利用 type guard (就是一堆 predicate 跟 if/else )協助我們確定這個 valuable 到底是什麼 type 。
補充
優先順序0為最低、9最高,預設的fixity為infixl 9
| Precedence | Left associative operators | Non-associative operators | Right associative operators |
|---|---|---|---|
| 9 | !! | . | |
| 8 | ^, ^^, ** | ||
| 7 | *, /, div, mod, rem, quot |
||
| 6 | +, - | ||
| 5 | :, ++ | ||
| 4 | ==, /=, <, <=, >, >=, elem, notElem |
||
| 3 | && | ||
| 2 | || | ||
| 1 | >>, >>= | ||
| 0 | $, $!, seq |
fixity的深入說明可以參考Fix(ity) me
感謝補充~