結合 sum 與 product 型態
February 5, 2022在〈從 tuple 到 product 型態〉初次接觸了簡單的 product 型態,這邊要來更進一步討論 sum 與 product 型態,以及彼此結合後的更多樣型態。
sum 型態
布林值在 Haskell 有 True
與 False
,型態是 Bool
,它的定義方式是:
data Bool = False | True deriving (Read, Show, Eq, Ord, Enum, Bounded)
先不用管 deriving
,稍後就會解釋,總之 Bool
型態是 True
或 False
,這種交替(alternation)、或(or)、非此既彼(either)構成的型態,類似集合 A + B 的概念(這邊的 +
代表互斥聯集)。
就目前來說,Bool
可表現的狀態顯然只有兩個,稍後會看到,這種交替構成的型態,可以與 product 型態結合,例如:
data CondFoo = CondA Bool Bool | CondB Bool Bool
CondFoo
可表現的狀態數量,就是 CondA Bool Bool
與 CondB Bool Bool
可表示狀態數量(各是 4 個)的總和,也就是 8 個狀態,因此稱為 sum 型態。
類似地,或許你想設計一個 2D 繪圖程式庫,在你的設計中,每個 2D 圖案都會是 Shape
型態,你會提供固定的幾個基本 2D 圖案幾何資訊,也許目前是三個吧!像是 Triangle
、Rectangle
與 Circle
,或許就可以這麼設計:
data Shape = Triangle | Rectangle | Circle
這麼一來,你就可以這麼使用 Triangle
、Rectangle
與 Circle
這三個值(嚴格來說,是三個無參數的值建構式,值建構式就是函式,Haskell 的函式就是值),而它們的型態會是 Shape
:
ghci> data Shape = Triangle | Rectangle | Circle
ghci> let c = Circle
ghci> :t c
c :: Shape
ghci> c
<interactive>:13:1: error:
‧ No instance for (Show Shape) arising from a use of ‘print’
‧ In a stmt of an interactive GHCi command: print it
ghci>
不過在 ghci
中無法直接顯示 Triangle
、Rectangle
與 Circle
?因為它們只是值,你還沒有定義這些值支援什麼行為,例如,在 ghci
中想顯示這些值的字串描述,至少要有 Show
行為,才能讓 show
函式處理。
目前還不會談到怎麼讓資料支援某些行為,然而 Haskell 可以讓資料型態從本身定義過的一些行為衍生,這就是方才看到的 deriving
語法的目的。例如:
ghci> data Shape = Triangle | Rectangle | Circle deriving Show
ghci> Circle
Circle
ghci> Rectangle
Rectangle
ghci> Triangle
Triangle
ghci>
product/sum 型態
如果在你的設計中,每個 2D 圖案都會有個幾何中心,為此可以定義一個 Point
型態:
data Point = Point Float Float deriving Show
這是個 product 型態,現在可以為 Triangle
、Rectangle
與 Circle
加入更多資訊了,例如,三角形擁有中心與邊長,長方形有中心、長、寬,圓形有中心、半徑:
data Shape = Triangle Point Float | Rectangle Point Float Float | Circle Point Float deriving Show
看到了嗎?sum 與 product 型態可以組合,這就有了更複雜結構的資料型態了,如果要建立一個圓呢?
ghci> let center = Point 0 0
ghci> let radius = 10
ghci> let c = Circle center radius
ghci> c
Circle (Point 0.0 0.0) 10.0
ghci>
因為定義型態,都是直接揭露了結構,想要拆解出其中資料,就可以透過模式比對了:
ghci> let Circle _ r = c
ghci> r
10.0
ghci> let Circle (Point x y) _ = c
ghci> x
0.0
ghci> y
0.0
ghci>
遞迴地定義型態
進一步地,也可以遞迴地定義型態,例如,來定義一個簡單的 List
,只能有 Int
的元素:
data List = Empty | Con Int List deriving Show
在以上的定義中,List
型態的值可以是 Empty
,或者是 Con Int List
,也就是一個 Int
與一個 List
組合而成的值,因此,可以這麼建立 Int
元素的 List
了:
ghci> let lt1 = Con 1 Empty
ghci> let lt2 = Con 2 lt1
ghci> let lt3 = Con 3 lt2
ghci> let lt4 = Con 3 $ Con 2 $ Con 1 Empty
ghci> let lt1 = Con 1 Empty
ghci> let lt2 = Con 2 lt1
ghci> let lt3 = Con 3 lt2
ghci> let lt4 = Con 3 $ Con 2 $ Con 1 Empty
ghci> lt1
Con 1 Empty
ghci> lt2
Con 2 (Con 1 Empty)
ghci> lt3
Con 3 (Con 2 (Con 1 Empty))
ghci> lt4
Con 3 (Con 2 (Con 1 Empty))
ghci>
這也就模仿了 Haskell 內建的 list 行為,Con
就相當於 :
,在上面的例子中,lt4
的建立方式,有沒有像是 3:2:1:[]
呢?那麼可以可以模仿 x:xs
模式比對的行為呢?
ghci> let Con x xs = lt4
ghci> x
3
ghci> xs
Con 2 (Con 1 Empty)
ghci>
不過,既然 Con
就相當於 :
,有沒有辦法像 :
這麼用呢?
ghci> 2 : 1 : []
[2,1]
ghci>
可以使用中序形式來定義值建構式,方式是在值建構式名稱前加個 :
,例如:
ghci> data List = Empty | Int :~ List deriving Show
ghci> 1 :~ Empty
1 :~ Empty
ghci> 2 :~ (1 :~ Empty)
2 :~ (1 :~ Empty)
ghci>
在這邊需要使用括號,這是因為你沒有為中序形式的值建構式定義結合時的優先權,預設會是左結合且優先權為 9,可以使用 :info
來確定:
ghci> :info :~
type List :: *
data List = ... | Int :~ List
-- Defined at <interactive>:3:21
infixl 9 :~
ghci>
如果需要改變優先權,可以使用 infixl
、infix
、infixr
,分別用來設定左結合、無結合、右結合優先權,例如原始碼中如下定義,讓 :~
為右結合,就可以使用 2 :~ 1 :~ Empty
的形式來建構 List
:
infixr 0 :~
data List = Empty | Int :~ List deriving Show
中序形式的值建構式不要濫用,除非真的對程式的撰寫便利性及可讀性有很大的幫助!
總之,只要能知道型態在定義時的結構,就能依結構來拆解其中的資料;當然,Haskell 內建的 list,元素可以是各種型態,這就需要知道如何定義型態參數了,這之後文件再來聊。