型態類別定義、實作與衍生

February 6, 2022

在之前的文件中已數次看過型態類別(Typeclass),具有某型態類別行為的型態,必須實現該型態類別規範的行為,那麼,要怎麼定義自己的型態類別?

舉個例子來說好了,你設計了一個快速排序法:

quicksort :: Comp a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = quicksort smaller ++ (x : quicksort larger)
    where smaller = [y | y <- xs, (comp x y) >= 0]
          larger = [y | y <- xs, (comp x y) < 0]

這邊要求傳進來的 list,元素都必須實現 Comp 規範的行為,也就是要實現 comp 函式,當然,其實可以使用 x >= yx < y,這邊只是為了示範才使用 comp 這個自訂行為。

定義、實作型態類別

要定義型態類別,必須使用 class 關鍵字,例如,定義方才的範例需要的 Comp

class Comp a where
    comp :: a -> a -> Int

這邊定義了一個 Comp 的型態類別,a 是型態參數,稍後你要定義它的具體型態;為了讓 Int 可以進行 quicksort [3, 2, 4, 7],你必須讓 Int 成為 Comp 的實例,這可以使用 instance 關鍵字來定義與實作:

instance Comp Int where
    comp x y = x - y

如果自定義了一個 Circle 型態,可指定中心 x、y 與半徑:

data Circle = Circle Float Float Float deriving Show

如果排序時想依半徑,那麼以下是個讓 Circle 成為 Comp 的實例的示範:

instance Comp Circle where
    comp (Circle _ _ r1) (Circle _ _ r2) 
        | d == 0 = 0
        | d > 0  = 1
        | d < 0  = -1
        where d = r1 - r2

現在就可以對 Circle 進行 quicksort 了:

class Comp a where
    comp :: a -> a -> Int
    
data Circle = Circle Float Float Float deriving Show

instance Comp Circle where
    comp (Circle _ _ r1) (Circle _ _ r2) 
        | d == 0 = 0
        | d > 0  = 1
        | d < 0  = -1
        where d = r1 - r2
        
quicksort :: Comp a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = quicksort smaller ++ (x : quicksort larger)
    where smaller = [y | y <- xs, (comp x y) >= 0]
          larger = [y | y <- xs, (comp x y) < 0]
          
main = do
    let circles = [Circle 0 0 3.0, Circle 1 1 2.0, Circle (-1) (-1) 5.0] 
        sorted = quicksort circles
    putStrLn $ show sorted

執行後會顯示半徑由小至大的排列:

[Circle 1.0 1.0 2.0,Circle 0.0 0.0 3.0,Circle (-1.0) (-1.0) 5.0]

如果你熟悉 Java,可能會覺得這過程像是在實作 interface,雖然這是個過渡語言經驗的聯想方式,不過要小心的是,Java 的 interface 用來實現次型態多型(subtype polymorphism);然而 Haskell 沒有物件的觀念,型態類別實現的是特定多型(ad hoc polymorphism),白話來說就是重載(overload),例如,以方才的 quicksort circles 來說,是在靜態時期就選定了 Circle 版本的 comp 實作。

內建的型態類別

ghci 中要顯示資料的描述時,資料必須具有 Show 的行為,這是個內建的型態類別,它有個 show 函式必須實作,型態是 Show a => a -> String,方才是直接讓 Circle 透過 deriving 衍生自預設的 Show 行為實作,如果想自行實現 Circleshow 函式,可以如下:

instance Show Circle where
    show (Circle x y r) = 
        "Circle(x: " ++ show x ++ ", y: " ++ show y ++ ", r: " ++ show r ++ ")"

刪除方才範例的 deriving Show,將以上的 Show 實作程式碼加入,編譯執行後就會顯示如下:

[Circle 1.0 1.0 2.0,Circle 0.0 0.0 3.0,Circle (-1.0) (-1.0) 5.0]

Haskell 中有幾個內建的型態類別,Show 是其中一例,另外還有 Eq 可用來比較兩個資料是否相等:

class Eq a where 
    (==) :: a -> a -> Bool 
    (/=) :: a -> a -> Bool 
    x == y = not (x /= y) 
    x /= y = not (x == y)

在這邊可以看到,Eq 型態類別定義了 ==/= 兩個行為必須實作,而本身也有兩個預設實作 x == yx /= y,如果沒有這兩個預設實作,那麼 Eq 實例對 ==/= 兩個行為都要實作。

而在上例中,因為 x == yx /= y 彼此互補實作,只要選擇其中 x == yx /= y 其中之一實作,另一個就自然具有對應的功能。例如,讓 Circle 可以依半徑比較相等或不相等:

instance Eq Circle where
    (Circle _ _ r1) == (Circle _ _ r2) = r1 == r2

這麼一來,像是 Circle 0 0 1 == Circle 1 1 1 就會是 True,而 Circle 0 0 1 /= Circle 1 1 1 就會是 False

來看看如果方才的 quicksort,如果改成這樣的話呢?

quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = quicksort smaller ++ (x : quicksort larger)
    where smaller = [y | y <- xs, x >= y]
          larger = [y | y <- xs, x < y]

Ord 是內建的型態類別,它的定義是 class Eq a => Ord a where,其中 Eq 做了型態約束,這使得 Ord 的具體型態必須也有 Eq 的行為,Ord 的行為包括了 <<=> >= 等,如果要實現 Ord 至少得實作 <=,其他的行為會以 <= 進行預設實作。

例如,Circle 想自行實現 Ord 的行為,可依半徑比序,可以像是:

instance Ord Circle where
    (Circle _ _ r1) <= (Circle _ _ r2) = r1 <= r2

記得,若沒有先讓 Circle 實現 Eq 的行為,那麼就會引發錯誤;現在可以讓 Circle 使用 quicksort 排序了:

data Circle = Circle Float Float Float

instance Show Circle where
    show (Circle x y r) = 
        "Circle(x: " ++ show x ++ ", y: " ++ show y ++ ", r: " ++ show r ++ ")"
        
instance Eq Circle where
    (Circle _ _ r1) == (Circle _ _ r2) = r1 == r2

instance Ord Circle where
    (Circle _ _ r1) <= (Circle _ _ r2) = r1 <= r2
        
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = quicksort smaller ++ (x : quicksort larger)
    where smaller = [y | y <- xs, x >= y]
          larger = [y | y <- xs, x < y]
          
main = do
    let circles = [Circle 0 0 3.0, Circle 1 1 2.0, Circle (-1) (-1) 5.0] 
        sorted = quicksort circles
    putStrLn $ show sorted

既然定義型態類別時可以設定型態約束,那麼實作型態約束的實例時可不可以呢?當然是可以的,舉例來說,Maybe 是可以比較相等性的,例如 Just 10 == Just 10 結果會是 True,來看看 Maybe 是怎麼定義為 Eq 的實例:

instance (Eq m) => Eq (Maybe m) where 
    Just x == Just y = x == y 
    Nothing == Nothing = True 
    _ == _ = False

在定義 MaybeEq 的實例時,必須能對 Maybe 中實際的值進行相等性比較,因此,m 也必須是 Eq 的實例,這就是上面的定義之意思。

型態類別衍生

Haskell 有內建的 ShowEqOrdReadEnumBounded 等型態類別,這些型態類別很常用,可以在定義型態時,使用 deriving 自動衍生實例,而不必自行使用 instance 來定義。

技術上來說,Haskell 的編譯器,會自動為產生這些型態類別實例的相關程式碼。例如:

data Customer = Customer String String Int deriving Show

這麼一來,若試著使用 show 來取得 Customer 值的描述,例如 show $ Customer "Justin" "Lin" 46,就會傳回 "Customer Justin Lin 46" 的字串,如果要衍生自多個型態類別,也是可以的,例如:

data Customer = Customer String String Int deriving (Show, Read)

show 相反,read 可以使用指定的字串與型態,將字串剖析為指定的型態值,例如在上面的例子中,你可以使用 read "Customer \"Justin\" \"Lin\" 46" :: Customer,這樣會建立 Customer Justin Lin 46 的值。

由於某些型態類別的衍生,會有型態約束,就像 Ord 會約束必須也具有 Eq 一樣,因此要能使用 readRead 的衍生,必須也具有 Show 的行為,因此,如果要使用 deriving 自動衍生 Ord,相對地,也要自動衍生 Eq

data Customer = Customer String String Int deriving (Show, Read, Eq, Ord)

Enum 是可列舉的型態類別,當你想將一組值作為列舉值,沒有特別要求的情況下,可以直接衍生,如此一來,你就可以透過 [..] 來列舉範圍,或者 succpred 來取得某個列舉值的下一個’或’上一個列舉值,例如:

ghci> data Action = Up | Down | Left | Right deriving (Show, Enum)
ghci> [Up ..]     
[Up,Down,Left,Right]
ghci> [Up .. Left]
[Up,Down,Left]
ghci> pred Down
Up
ghci> succ Down
Left
ghci>

Bounded 是定義上下界的型態類別,可搭配 minBoundmaxBound 函式使用,這麼一來,就可以透過上下界指定來做些操作:

ghci> data Action = Up | Down | Left | Right deriving (Show, Enum, Bounded)
ghci> minBound :: Action
Up
ghci> maxBound :: Action
Right
ghci> [minBound .. maxBound] :: Action

<interactive>:15:1: error:
    ‧ Couldn't match expected type ‘Action’ with actual type ‘[a0]’
    ‧ In the expression: [minBound .. maxBound] :: Action
      In an equation for ‘it’: it = [minBound .. maxBound] :: Action
ghci> [minBound .. maxBound] :: [Action]
[Up,Down,Left,Right]
ghci>

如果想知道型態類別有哪些衍生的實例,可以使用 :info

ghci> :info Num
type Num :: * -> Constraint
class Num a where
  (+) :: a -> a -> a
  (-) :: a -> a -> a
  (*) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  {-# MINIMAL (+), (*), abs, signum, fromInteger, (negate | (-)) #-}
        -- Defined in ‘GHC.Num’
instance Num Word -- Defined in ‘GHC.Num’
instance Num Integer -- Defined in ‘GHC.Num’
instance Num Int -- Defined in ‘GHC.Num’
instance Num Float -- Defined in ‘GHC.Float’
instance Num Double -- Defined in ‘GHC.Float’
ghci>

這邊也看到了 Num 的行為規範,包含了 +-*negate 等數字運算,這也就是為什麼,當函式中需要加減等運算時,會需要 Num 的實例。

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