Haskell Tutorial(14)減輕型態負擔的型態參數 << 前情
Typeclass、Typeclass,在之前的文件中,你已數次看過 Typeclass,具有某個 Typeclass 行為的型態,必須實現該 Typeclass 規範的行為,那麼,要怎麼定義自己的 Typeclass?
舉個例子來說好了,你設計了一個快速排序法:
quicksort :: Comp a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = quicksort small ++ (x : quicksort large)
where small = [y | y <- xs, (comp x y) >= 0]
large = [y | y <- xs, (comp x y) < 0]
你要求傳進來的 List,元素都必須實現 Comp 規範的行為,也就是 comp 函式,當然,其實可以使用 x >= y 或 x < y ,這邊只是為了示範才使用 comp 這個自訂行為。
定義 Typeclass
要定義 Typeclass,必須使用 class 關鍵字,例如,定義方才的範例需要的 Comp :
class Comp a where
comp :: a -> a -> Integer
在這邊,class 表示定義 Typeclass,而不是物件導向中的類別,例如,如果你熟悉 Java,這邊的 class 與 Java 的 class 沒有關係,反而是與 interface 比較相近。
(用 Java 的 interface 比喻,比較容易理解 Typeclass 在規範與限制行為上的作用,實際上,在實現 Typeclass 實例時,倒是比較類似 Java 中 overloading 的概念。)
這邊定義了一個 Comp 的 Typeclass,a 是型態參數,稍後你要定義它的具體型態,如果你對 comp 函式檢驗型態,它的型態會是 Comp a => a -> a -> Integer 。
實作 Typeclass
那麼,為了讓 quicksort [3, 2, 4, 7] ,你必須讓 Integer 成為 Comp 的實例,這可以使用 instance 關鍵字來定義與實作:
instance Comp Integer where
comp x y = x - y
如果你自定義了一個 Circle 型態,可指定半徑與顏色:
data Circle = Circle Integer String
如果比序時是依半徑,那麼以下是個讓 Circle 成為 Comp 的實例的示範:
instance Comp Circle where
comp (Circle r1 _) (Circle r2 _) = r1 - r2
現在,你可以對 Circle 進行 quicksort 了:
內建的 Typeclass
方才的例子中,因為你沒有定義 Circle 的描述方式,如果你想在 GHCI 中直接顯示 [circle | circle <- quicksort circles] 會發生錯誤:
這是因為 GHCI 中要顯示資料的描述時,你的資料必須具有 Show 這個 Typeclass 的行為,這是個內建的 Typeclass,它有個 show 函式必須實作,型態是 Show a => a -> String ,因此,如果你可以讓 Circle 實現 show 的行為:
instance Show Circle where
show (Circle r c) =
"Circle(radius: " ++ show r ++ ", color: " ++ c ++ ")"
現在你可以直接在 GHCI 中顯示 Circle 的描述了:
Haskell 中有幾個內建的 Typeclass,例如 Eq ,可用來比較兩個資料是否相等:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
在這邊可以看到,Eq 這個 Typeclass 定義了 == 與 /= 兩個行為必須實作,而本身也有兩個預設實作 x == y 與 x /= y ,如果沒有這兩個預設實作,那麼你的 Eq 實例對 == 與 /= 兩個行為都要實作,而上例中,因為 x == y 與 x /= y 彼此互補實作,因此,只要選擇其中 x == y 或 x /= y 其中之一實作,另一個就自然具有對應的功能。例如,讓 Circle 可以依半徑比較相等或不相等:
instance Eq Circle where
(Circle r1 _) == (Circle r2 _) = r1 == r2
這麼一來,像是 Circle 10 "Red" == Circle 10 "Black" 就會是 True ,而 Circle 10 "Red" /= Circle 10 "Black" 就會是 False 。
來看看如果方才的 quicksort ,如果改成這樣的話呢?
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = quicksort small ++ (x : quicksort large)
where small = [y | y <- xs, x >= y]
large = [y | y <- xs, x < y]
Ord 是內建的 Typeclass,它的定義是 class Eq a => Ord a where ,其中 Eq 做了型態約束,這使得 Ord 的具體型態必須也有 Eq 的行為,Ord 的行為包括了 < 、<= 、> 、>= 等,如果你要實現 Ord 至少得實作 <= ,其他的行為會以 <= 進行預設實作。
(class Eq a => Ord a where 這樣的作法,某些程度上,就類似 Java 中 interface 的繼承,稍微對比一下的話,就像是 public interface Ord extends Eq 的定義。)
例如,想讓 Circle 實現 Ord 的行為,可依半徑比序,可以像是:
instance Ord Circle where
(Circle r1 _) <= (Circle r2 _) = r1 <= r2
注意,如果你沒有先讓 Circle 實現 Eq 的行為,那麼就會引發錯誤;現在可以讓 Circle 使用 quicksort 排序了:
既然談到了定義 Typeclass 可以設定型態約束,那麼,實作 Typeclass 的實例時可不可以呢?當然是可以的,舉例來說,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
在定義 Maybe 為 Eq 的實例時,必須能對 Maybe 中實際的值進行相等性比較,因此,m 也必須是 Eq 的實例,這就是上面的定義之意思。
可 deriving 的 Typeclass
Haskell 有內建的 Show 、Eq 、Ord 、Read 、Enum 、Bounded 等 Typeclass,這些 Typeclass 很常用,你可以在定義型態時,使用 deriving 自動衍生出實例,而不必自行使用 instance 來定義,技術上來說,Haskell 的編譯器,會自動為你產生這些內建的 Typeclass 實例的相關程式碼。例如:
data Customer = Customer String String Int deriving Show
這麼一來,如果你試著使用 show 來取得 Customer 值的描述,例如 show $ Customer "Justin" "Lin" 39 ,就會傳回 "Customer Justin Lin 39" 的字串,如果要衍生自多個 Typeclass,也是可以的,例如:
data Customer = Customer String String Int deriving (Show, Read)
跟 show 相反,read 可以使用指定的字串與型態,為你建立值,例如在上面的例子中,你可以使用 read "Customer \"Justin\" \"Lin\" 10" :: Customer ,這樣會建立 Customer Justin Lin 39 的值。
由於某些 Typeclass 的衍生,會有型態約束,就像 Ord 會約束必須也具有 Eq 一樣,因此,要能使用 read ,Read 的衍生,必須也具有 Show 的行為,因此,如果要使用 deriving 自動衍生 Ord ,相對地,也要自動衍生 Eq :
data Customer = Customer String String Int deriving (Show, Read, Eq, Ord)
Enum 是可列舉的 Typeclass,例如:
data TouchSensor = Released | Pressed | Bumped deriving (Show, Enum)
這麼一來,你就可以直接列舉 TouchSensor 的值,例如:
Bounded 是定義上下界的 Typeclass,可搭配 minBound 、maxBound 函式使用,例如:
data TouchSensor = Released | Pressed | Bumped deriving (Show, Enum, Bounded)
這麼一來,你就可以直接取得上下界:
如果你想知道,Typeclass 有哪些衍生的實例,可以使用 :info 。
最後來個題目吧!先回到一開始的那個 Comp 與 quicksort :
class Comp a where
comp :: a -> a -> Integer
quicksort :: Comp a => [a] -> [a]
quicksort [] = []
quicksort (x:xs) = quicksort small ++ (x : quicksort large)
where small = [y | y <- xs, (comp x y) >= 0]
large = [y | y <- xs, (comp x y) < 0]
該怎麼定義 Maybe 為 Comp 的 instance ,才能有以下的結果呢?
後續 >> Haskell Tutorial(16)Record 語法、Type 同義詞
|