結合 sum 與 product 型態

February 5, 2022

在〈從 tuple 到 product 型態〉初次接觸了簡單的 product 型態,這邊要來更進一步討論 sum 與 product 型態,以及彼此結合後的更多樣型態。

sum 型態

布林值在 Haskell 有 TrueFalse,型態是 Bool,它的定義方式是:

data Bool = False | True deriving  (Read, Show, Eq, Ord, Enum, Bounded)

先不用管 deriving,稍後就會解釋,總之 Bool 型態是 TrueFalse,這種交替(alternation)、或(or)、非此既彼(either)構成的型態,類似集合 A + B 的概念(這邊的 + 代表互斥聯集)。

就目前來說,Bool 可表現的狀態顯然只有兩個,稍後會看到,這種交替構成的型態,可以與 product 型態結合,例如:

data CondFoo = CondA Bool Bool | CondB Bool Bool

CondFoo 可表現的狀態數量,就是 CondA Bool BoolCondB Bool Bool 可表示狀態數量(各是 4 個)的總和,也就是 8 個狀態,因此稱為 sum 型態。

類似地,或許你想設計一個 2D 繪圖程式庫,在你的設計中,每個 2D 圖案都會是 Shape 型態,你會提供固定的幾個基本 2D 圖案幾何資訊,也許目前是三個吧!像是 TriangleRectangleCircle,或許就可以這麼設計:

data Shape = Triangle | Rectangle | Circle

這麼一來,你就可以這麼使用 TriangleRectangleCircle 這三個值(嚴格來說,是三個無參數的值建構式,值建構式就是函式,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 中無法直接顯示 TriangleRectangleCircle?因為它們只是值,你還沒有定義這些值支援什麼行為,例如,在 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 型態,現在可以為 TriangleRectangleCircle 加入更多資訊了,例如,三角形擁有中心與邊長,長方形有中心、長、寬,圓形有中心、半徑:

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>

如果需要改變優先權,可以使用 infixlinfixinfixr,分別用來設定左結合、無結合、右結合優先權,例如原始碼中如下定義,讓 :~ 為右結合,就可以使用 2 :~ 1 :~ Empty 的形式來建構 List

infixr 0 :~
data List = Empty | Int :~ List deriving Show

中序形式的值建構式不要濫用,除非真的對程式的撰寫便利性及可讀性有很大的幫助!

總之,只要能知道型態在定義時的結構,就能依結構來拆解其中的資料;當然,Haskell 內建的 list,元素可以是各種型態,這就需要知道如何定義型態參數了,這之後文件再來聊。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter