現代開發者談到多型(Polymorphism),多半聯想到物件導向程式設計,像是使用Java的類別繼承或介面來實現,而純函數式的Haskell中也有多型。

從多型的角度,回頭去看看型態類別(Typeclass)與型態參數(Type parameter),就不會覺得這類元素特別神祕難解了!

型態類別與特定多型

在Haskell中有型態類別,而且,可使用class關鍵字來定義。

由於它與多數主流物件導向程式語言定義類別時,同樣使用class作為關鍵字,因此常令初接觸Haskell的開發者混淆困惑。

實際上,就定義而言,定義型態類別的目的,在於規範與限制資料的行為,這點與Java的interface相近。

然而,在使用instance關鍵字定義型態類別的實例時,卻是在實現特定(Ad-hoc)多型,這一點,倒是類似Java中的重載(Overloading)實現了。

舉例來說,如果在Haskell中想測試元素是否在某個清單,可以設計一個elem函式:

x `elem`  []     = False

x `elem` (y:ys)  = x == y || (x `elem` ys)

在這個elem函式定義中,空清單傳回False,或者使用元素x與清單首元素y逐一做==相等比較,這個elem函式並非能適用各種元素型態之清單,因為元素的型態必須能適用==函式,這行為可以使用型態類別定義出來:

class Eq a where

  (==) :: a -> a -> Bool

你可以在elem函式前,加上函式的型態宣告elem :: Eq a => a -> [a] -> Bool,這表示只有實現了Eq的==行為之資料,才能作為引數傳給elem,假設你有個Customer String Int作為元素的清單,現在想要測試清單中是否具有某客戶資料,那麼可以定義Customer為Eq的實例:

instance Eq Customer where

  (Customer n1 a1) == (Customer n2 a2) = n1 == n2 && a1 == a2 

如果定義的型態類別中有多個行為必須實作,定義型態類別實例時,也必須逐一實現。

若延續Java的interface比喻,使用instance來定義型態類別實例,就像是在實作interface,而就個別的行為本身,實際上是在進行函式重載,像上例就是在重載==函式,使之也能適用Customer。

重載是比較為人熟知的名稱,實際上就是在實現特定多型,也就是讓同一個函式,可以因不同型態而有各異的實作。

型態參數與參數多型

在Haskell中,如果想設計一個swapInt :: (Int, Int) -> (Int, Int),可以將Tuple中兩個Int元素對調,同時又設計了一個swapFloat :: (Float, Float) -> (Float, Float),可以將Tuple中兩個Float元素對調,如此,馬上就可以發現swapInt與swapFloat的函式實作,都是(x, y) = (y, x),並沒有因不同參數型態,而有不同實作,然而,在Haskell中,可以使用型態參數來解決這個問題:

swap :: (a, b) -> (b, a)

swap (x, y) = (y, x)

如此一來,不僅Int、Float,任何型態都可以運用這個swap函式,a、b是型態參數,實際型態可由編譯器推斷或自行指定。

而這麼做,也就是允許不同參數型態能有相同實作,正好與不同型態可有各異實作的特定多型,分別座落於天平的兩端。

熟悉Java的開發者可能,會因此聯想到泛型(Generics),也會想到可用extends進行型態約束,實際上,方才elem的型態宣告Eq a => a -> [a] -> Bool中的Eq a =>,如果用Java泛型語法來比擬的話,就會像是<T extends Eq>。

由於Haskell不支援物件導向,型態之間不會有複雜的繼承關係,因此,不會有泛型擁有的那些正變性(Covariance)、逆變性(Contravariance)等複雜元素存在,因而型態參數在使用上單純許多。

又由於可使用一致的函式介面來處理不同的資料型態,像是swap這類函式可適用各種型態——這是多型的一種實現,稱之為參數(Parametric)多型,在Haskell中,隨處可見型態參數之運用,因此,在Haskell中談到多型兩字之時,多半指的就是參數多型,而swap這類函式就直接被稱為多型函式。

模擬次型態多型

過去我曾在〈多型的本質〉主標題下,以三篇專欄文章分別探討過特定(Ad-hoc)多型、參數(Parametric)多型與次型態(Subtype)多型,也使用了Java,來示範這三種多型的實現。

既然,現在知道了Haskell的型態類別可實現特定多型,而型態參數可實現參數多型,那麼,有沒有實現次型態多型的方式呢?

Haskell並非具備物件導向典範的語言,不能直接實現次型態多型,然而,視實際需求而言,仍可透過一些設計來做特定的模擬。

舉例來說,若有型態Customer與Vip定義如下:

data Customer = Customer String Int

data Vip = Vip Customer Float

這相當於Vip繼承了Customer且多了折扣率,可以定義型態類別class CustBehavior c來規範Customer應有的行為,而class CustBehavior c => VipBehavior c用以規範Vip的行為,亦即屆時VipBehavior實例也必須是CustBehavior的實例。

這就好比Java中進行了interface繼承,稍微用語法對比一下的話,就像是interface VipBehavior extends CustBehavior的定義。

現在可讓Customer與Vip都定義為CustBehavior的實例,而只有Vip定義為VipBehavior的實例,以模擬行為的繼承與實現,如果有個函式在型態宣告上,使用了CustBehavior a =>來限制可適用的引數型態,那麼Customer與Vip都可以傳入該函式,這就相當於模擬了次型態多型(程式碼可參考Gist:http://goo.gl/P5kKJN)。

實際上,在Haskell中模擬次型態多型,沒什麼太實質的效益,由於缺少次型態的直接支援,這類模擬在程式碼撰寫上,冗長且痛苦。

真的想要同時具有函數式與物件導向典範,選擇一門已同時考量兩種典範的語言,像是Scala,會是一個比較好的選擇。

瞭解語言的意義而非語法

之所以會有以上這一連串的思考過程,主要是來自於自己對Haskell想得不夠透徹的某個下午。

由於一開始過於著重語法,而被Haskell的class Eq a => Ord a、甚至instance (Eq m) => Eq (Maybe m)給迷惑了,直到後來看了〈A Gentle Introduction to Haskell〉中的〈Type Classes and Overloading〉,以及HaskellWiki中的〈OOP vs type classes〉,從多型的角度重新回頭看那些語法的意義,我那時才豁然開朗。

有時,這是深入一門語言時難免的過程,特別是在腦袋還忙著建立新語言的語法規則之時,試著重新思考這類語法存在之意義,像是從使用一致介面來處理不同資料型態的多型角度,重新思考Haskell的型態類別與型態參數,往往就能擺脫被語法困住的窘境,進一步融會、類比或區別各語言間相同與相異之設計!

作者簡介


Advertisement

更多 iThome相關內容