從 tuple 到 product 型態
February 4, 2022在先前的文件中,使用到的數字、list 等型態,都是 Haskell 預先定義好的代數資料型態(Algebraic data type),代數資料型態是基於資料的結構來構成型態,新型態的構成來自於型態與型態的結合,因而拿到一個資料時,就能利用其結構的規律性來進行處理。
先前的文件中看過不少 list 的處理模式,基本上就是利用了結構的規律性,來發掘出流程的規律性,從而能抽取出流程的抽象。
在 Haskell 中,會基於 product 型態與 sum 型態來構造新型態,這一篇文件會先從 tuple 開始介紹, 從中引出 product 型態的需求。
認識 tuple
跟 list 類似,tuple 也是個容器,可以 ()
來建立一個 tuple,例如使用 (1, 2, 3)
建立內含數字 1
、2
、3
的 tuple,或者是使用 (1, "Justin Lin", 89.1)
建立內含 1
、"Justin Lin"
與 89.1
的 tuple,這是初學者最容易看出 tuple 與 list 的不同之處,list 中的元素必須是同一型態,而 tuple 的元素可以有各自不同型態。
有些函式會產生 tuple,例如說 zip
,你可以給它兩個 list,元素會被兩兩配對,產生一個內含 tuple 的 List:
ghci> zip ["Justin", "Monica", "Irene"] [98, 100, 99]
[("Justin",98),("Monica",100),("Irene",99)]
ghci>
如果是成對的 tuple,可以使用 fst
來取第一個元素,用 snd
來取第二個元素:
ghci> fst ("Justin",98)
"Justin"
ghci> snd ("Justin",98)
98
ghci>
若是有三個以上元素的 tuple 呢?可以用模式比對:
ghci> let (name, score, rank) = ("Justin", 98, 'A')
ghci> name
"Justin"
ghci> score
98
ghci> rank
'A'
ghci>
如果你使用 :t
來測試,name
會是 String
、score
是 Num b => b
,而 rank
是 Char
,而 tuple 實例的型態,就是各個元素的型態結合而成,只不過沒有型態名稱:
ghci> :t ("Justin", 98, 'A')
("Justin", 98, 'A') :: Num b => (String, b, Char)
ghci>
Haskell 中沒有單元素的 tuple,這大概是原因之一,畢竟若 tuple 只有一個元素,例如 "Justin"
的話,tuple 的型態大概會是 (String)
之類,那麼為何不直接用 "Justin"
好呢?
Python 中也有 tuple 這種東西,可以使用 ('Justin',)
來建立內含一個元素的 tuple,與 list 不同的地方於 tuple 不可變動(immutable);然而,Haskell 本來就沒有可變動的概念,如果你真的要能夠內含一個元素的容器,或許就使用 list 吧!
函式可以接受 tuple 或傳回 tuple,因為 tuple 本身有型態,這會是函式型態的一部份:
ghci> let move point vector = (fst point + fst vector, snd point + snd vector)
ghci> move (10, 20) (0.5, 0.5)
(10.5,20.5)
ghci> move (10, 20) (0.5, 0.5, 0.25)
<interactive>:26:15: error:
‧ Couldn't match expected type: (a, b)
with actual type: (a0, b0, c0)
‧ In the second argument of ‘move’, namely ‘(0.5, 0.5, 0.25)’
In the expression: move (10, 20) (0.5, 0.5, 0.25)
In an equation for ‘it’: it = move (10, 20) (0.5, 0.5, 0.25)
‧ Relevant bindings include
it :: (a, b) (bound at <interactive>:26:1)
ghci> :t move
move :: (Num a, Num b) => (a, b) -> (a, b) -> (a, b)
ghci>
這從定義 move
是個可以從點 point
根據指定向量 vector
移動後,傳回新點座標的函式,Haskell 自動推斷出 move
必須是個能接受兩個 Num
行為元素的 Tuple,你傳入 (1, 1, 3)
就會引發錯誤。
顯然地,雖然你沒有明確地定義 Point
之類的型態,然而 (10, 20)
是被當成一個點座標來使用了,雖然你沒有明確地定義 Vector
之類的型態,然而 (0.5, 0.5)
是被當成向量來使用了。
在這邊可以看出 tuple 的作用之一,就是作為簡便的資料載體(data carrier),用來臨時承載一些資料,簡便是好處也是壞事,例如,如果我寫了 (1, 2)
,這是點座標還是向量呢?(10, 20) == (10, 20)
的結果會是 True
,如果我說 ==
左邊是點座標右邊是向量,那這個 True
是對的嗎?
方才我寫了 ("Justin", 98, 'A')
,這是什麼型態的資料呢?學生?球員?
定義 product 型態
顯然地,當你面對方才的幾個問號時,就表示使用 tuple 不再足夠了,你需要為這一組資料定義型態名稱,以便能從進一步從型態名稱上來區別資料,而不是只從元素的型態組合來區別資料。
例如,你可以定義 Point
、Vector
型態:
data Point = Pt Float Float
data Vector = Vt Float Float
data
用來定義資料型態,Point
是型態名稱,而 Pt
是值建構式(Value constructor),如果不影響可讀性,值建構式可以與型態名稱同名,值建構式之後這個型態的各個值域(field)型態。
就以上簡單的型態定義來說,就可以進行模式比對:
hci> data Point = Pt Float Float
ghci> data Vector = Vt Float Float
ghci> let p = Pt 10 20
ghci> let v = Vt 10 20
ghci> let Pt px py = p
ghci> let Vt vx vy = v
ghci> :t p
p :: Point
ghci> :t v
v :: Vector
ghci> px
10.0
ghci> py
20.0
ghci> vx
10.0
ghci> vy
20.0
ghci> let Vt vx vy = p
<interactive>:79:5: error:
‧ Couldn't match expected type ‘Point’ with actual type ‘Vector’
‧ In the pattern: Vt vx vy
In a pattern binding: Vt vx vy = p
ghci>
只要值建構式、值域的結構符合,就可以進行模式比對,從以上的結果中可以看到,p
與 v
是兩種不同的型態,想用 Vt vx vy
來拆解 p
是行不通的!
像以上這樣定義的 Point
、Vector
型態,稱為 product 型態,會這麼稱呼的原因,是因為型態來自於多個型態的 direct product,也就是多個型態的結合(combination),A B 意謂著 A 與 B 結合為一個型態,而型態可表示的狀態數量,會是值域型態可表示的狀態數量之乘積(product)。
就這邊的例子而言,Float
代表浮點數,數會有無限多個,兩個 Float
組成的 Point
或 Vector
可表示的點或向量也就有無限多個。
如果是以下的 Condition
:
ghci> data Condition = Condition Bool Bool
ghci> let cond1 = Condition False False
ghci> let cond2 = Condition False True
ghci> let cond3 = Condition True False
ghci> let cond4 = Condition True True
由於 Bool
可表示的狀態只有 True
、False
,Condition
就只會有 2 乘 2 個可表示的狀態,也就是上面看到的四個狀態。
當然,這邊的 Point
、Vector
能做的事還不多,而 Haskell 還可以定義 sum 型態,或者藉由 product 型態與 sum 型態的各種組合,來滿足各種不同的型態需求,這就留待後續文件再來介紹了…