Haskell Tutorial(13)正式入門代數資料型態


在〈Haskell Tutorial(10)從 Tuple 初試模式比對〉中,稍微探討了自定型態的需求,其中一開始使用了 Tuple 來暫時解決 List 的需求,而後發覺需要專屬型態的需求,這是發掘自訂型態需求的一種過程。

在使用 Tuple 模擬 List 的過程中,想發覺到處理這些問題,基本上都有取清單首元素與尾清單這個動作,在 Haskell 中是相對容易的,或者也可以說一開始就被強迫去發覺這種模式,因為 Haskell 中狀態不可變動(Immutable),實際上,List 的資料型態在定義上,呈現出遞迴,初接觸 Haskell 的自訂型態,遞迴地定義資料型態可能比較難一些,因此,這邊還是從簡單的開始。

定義 sum 型態

來考慮一個需求吧!你正在設計一個機器人,上頭有個觸碰感應器,你經常得處理觸碰感應器狀態為鬆開、壓下或碰撞的情況,一開始你使用 012 來代表這三個狀態:

touchSensor :: Int -> Int
touchSensor 0 = ... -- 計算某值
touchSensor 1 = ... -- 計算某值
touchSensor 2 = ... -- 計算某值
touchSensor _ = ... -- 傳回一個值代表沒這個狀態 

問題在於,Int 的值範圍很多,你不得不設一個 touchSensor _ 來比對其 012 以外的其他狀態,沒有那個 touchSensor _ 定義的話,程式碼 touchSensor 3 會引發執行時期錯誤。

為了解決問題,可以自定義一個 TouchSensor 型態:

data TouchSensor = Released | Pressed | Bumped

關鍵字 data 表示要定義一個新的型態 TouchSensor,它的值可能是 ReleasedPressedBumpedReleasedPressedBumped 在 Haskell 中稱為值建構式(Value constructor),規定首字母必須大寫。

這麼一來,touchSensor 函式可以改寫為:

touchSensor :: TouchSensor -> Int
touchSensor Released = ... -- 計算某值
touchSensor Pressed  = ... -- 計算某值
touchSensor Bumped   = ... -- 計算某值

這麼一來,傳入 ReleasedPressedBumped 以外的值給 touchSensor,在編譯時期就會抓出這個錯誤,就函式定義本身的可讀性來說也比較好,如果在 GHCI 中使用 :t Released,會顯示 Released :: TouchSensor:: 右邊表示了 Released 的型態。

檢視型態資訊

由於 TouchSensor 的值可能是 ReleasedPressedBumped 之一,彼此之間可替代(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 時,ReleasedPressedBumped 都是函式,Released :: TouchSensor 中,Released 是函式名稱,而 TouchSensor 是傳回型態,這與你定義一個函式 test = 1:t test 會顯示 test :: Integer 類似。

那麼,Customer 型態的 Customer 值構造式型態是什麼呢?

檢視型態資訊

可以看到,Customer 接受型態為 StringStringInt 的三個引數,然後傳回 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 ListCon 是值構造式,接受 IntList 型態,也就是一個 Int 與一個 List 組合而成的值,因此,可以這麼建立 Int 元素的 List 了:

使用自訂的 List

像以上幾個定義出來的型態,被稱為代數資料型態(Algebraic data type),是因為型態的實際值,就是由 sum 與 product 兩類代數操作來構造而成,就如同其他程式語言中,自訂型態的需求並不是憑空而來,Haskell 中也自有其定義型態的前置需求,就像以上的幾個示範。

瞭解了代數資料型態的定義是怎麼來的之後,之後就要來認識一些語法細節,讓代數資料型態在定義或使用上能夠更為方便了 …