在〈Haskell Tutorial(10)從 Tuple 初試模式比對〉中,稍微探討了自定型態的需求,其中一開始使用了 Tuple 來暫時解決 List 的需求,而後發覺需要專屬型態的需求,這是發掘自訂型態需求的一種過程。
在使用 Tuple 模擬 List 的過程中,想發覺到處理這些問題,基本上都有取清單首元素與尾清單這個動作,在 Haskell 中是相對容易的,或者也可以說一開始就被強迫去發覺這種模式,因為 Haskell 中狀態不可變動(Immutable),實際上,List 的資料型態在定義上,呈現出遞迴,初接觸 Haskell 的自訂型態,遞迴地定義資料型態可能比較難一些,因此,這邊還是從簡單的開始。
定義 sum 型態
來考慮一個需求吧!你正在設計一個機器人,上頭有個觸碰感應器,你經常得處理觸碰感應器狀態為鬆開、壓下或碰撞的情況,一開始你使用 0
、1
、2
來代表這三個狀態:
touchSensor :: Int -> Int
touchSensor 0 = ... -- 計算某值
touchSensor 1 = ... -- 計算某值
touchSensor 2 = ... -- 計算某值
touchSensor _ = ... -- 傳回一個值代表沒這個狀態
問題在於,Int
的值範圍很多,你不得不設一個 touchSensor _
來比對其 0
、1
、2
以外的其他狀態,沒有那個 touchSensor _
定義的話,程式碼 touchSensor 3
會引發執行時期錯誤。
為了解決問題,可以自定義一個 TouchSensor
型態:
data TouchSensor = Released | Pressed | Bumped
關鍵字 data
表示要定義一個新的型態 TouchSensor
,它的值可能是 Released
、Pressed
或 Bumped
,Released
、Pressed
或 Bumped
在 Haskell 中稱為值建構式(Value constructor),規定首字母必須大寫。
這麼一來,touchSensor
函式可以改寫為:
touchSensor :: TouchSensor -> Int
touchSensor Released = ... -- 計算某值
touchSensor Pressed = ... -- 計算某值
touchSensor Bumped = ... -- 計算某值
這麼一來,傳入 Released
、Pressed
或 Bumped
以外的值給 touchSensor
,在編譯時期就會抓出這個錯誤,就函式定義本身的可讀性來說也比較好,如果在 GHCI 中使用 :t Released
,會顯示 Released :: TouchSensor
,::
右邊表示了 Released
的型態。
由於 TouchSensor
的值可能是 Released
、Pressed
或 Bumped
之一,彼此之間可替代(alternation),因此 TouchSensor
是一種聯集型態,又稱為 sum 型態。
定義 product 型態
來考慮另一個需求,你在建立一個客戶關係管理系統,目前客戶會有的基本資料有名稱、姓氏、年齡,這沒辦法使用 List,因為 List 中的元素型態要相同,因此你想到了可以使用 Tuple,假設你要定義一個 descCustomer
函式顯示客戶基本資料:
descCustomer :: (String, String, Int) -> String
descCustomer (firstName, lastName, age) =
"Customer(" ++ firstName ++ ", "
++ lastName ++ ", "
++ show age ++ ")"
如〈Haskell Tutorial(10)從 Tuple 初試模式比對〉中談到,Tuple 實際上建立了一個沒有名稱的型態,因此 (String, String, Int)
可以直接用於函式宣告,Tuple 本身也可以模式匹配,真不錯,descCustomer ("Justin", "Lin", 39)
可以傳回 "Customer(Justin, Lin, 39)"
,只是說,如果老是 ("Justin", "Lin", 39)
傳來傳去的,有時難免搞不清楚這到底是什麼資料?是客戶?還是他家的小狗?來為這個需求定義專屬型態好了:
data Customer = Customer String String Int
關鍵字 data
定義了一個 Customer
型態,而在 =
右邊的 Customer
是值構造式,在定義型態時,型態名稱是可以與值構造式同名,右邊的 Customer
值構造式中有三個項(Field),表示 Customer
型態的值會是由值結合(combination )而成,例如,你可以使用 Customer "Justin" "Lin" 39
來建立一個 Customer
的值,這麼一來,descCustomer
可以改寫如下:
desc :: Customer -> String
desc (Customer firstName lastName age) =
"Customer(" ++ firstName ++ ", "
++ lastName ++ ", "
++ show age ++ ")"
實際上,值構造式是個函式,之前定義 TouchSensor
時,Released
、Pressed
或 Bumped
都是函式,Released :: TouchSensor
中,Released
是函式名稱,而 TouchSensor
是傳回型態,這與你定義一個函式 test = 1
,:t test
會顯示 test :: Integer
類似。
那麼,Customer
型態的 Customer
值構造式型態是什麼呢?
可以看到,Customer
接受型態為 String
、String
與 Int
的三個引數,然後傳回 Customer
型態的值,現在 desc
只能接受 Customer
型態的值,不能接受 Tuple 了,既然值構造式是個函式,表示你也可以對它進行部份套用等函式可以做的事。
像 Customer
這樣的型態,它的值是由三個型態結合而成,又稱為 product 型態,而這也顯示了一個事實,在 Haskell 中,函式也會是型態定義之一,也就是說,值構造式也會是型態定義之一。
定義 sum 與 product 型態
在定義型態時,sum 與 product 可以同時使用,例如,如果你有一個顏色感應器,可以感受紅、黃、綠三種光的顏色,並量得光的強度,也許可以這麼定義:
data ColorSensor = Red Int | Yellow Int | Green Int
進一步地,也可以遞迴地定義型態,例如,如〈Haskell Tutorial(10)從 Tuple 初試模式比對〉中談到的,以下先來定義一個只能接受 Int
的 List:
data List = Empty | Con Int List
在以上的定義中,List
型態的值可以是 Empty
,或者是 Con Int List
,Con
是值構造式,接受 Int
與 List
型態,也就是一個 Int
與一個 List
組合而成的值,因此,可以這麼建立 Int
元素的 List
了:
像以上幾個定義出來的型態,被稱為代數資料型態(Algebraic data type),是因為型態的實際值,就是由 sum 與 product 兩類代數操作來構造而成,就如同其他程式語言中,自訂型態的需求並不是憑空而來,Haskell 中也自有其定義型態的前置需求,就像以上的幾個示範。
瞭解了代數資料型態的定義是怎麼來的之後,之後就要來認識一些語法細節,讓代數資料型態在定義或使用上能夠更為方便了 …