跳到主要內容
黯羽輕揚每天積累一點點

深入 typeclass_Haskell 筆記 4

免費2018-05-12#Functional_Programming#Haskell typeclass#Haskell Functor#Haskell类型类#Haskell kind#Haskell类型的类型#Haskell函子

很有意思的型別系統

零.Typeclass 與 Class

Typeclass 就是 Haskell 中的介面定義,用來宣告一組行為

OOP 中的 Class 是物件模板,用來描述現實事物,並封裝其內部狀態。FP 中沒有內部狀態一說,所以Class 在函式式上下文指的就是介面。派生自某類別( deriving (SomeTypeclass) )是說具有某類別定義的行為,相當於 OOP 中的實現了某個介面,所以具有介面定義的行為

一.宣告

class 關鍵字用來定義新的 typeclass:

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

其中, a 是個型別變數,在定義 instance 時給出具體型別。前兩條型別宣告是介面所定義的行為(透過定義函式型別來描述)。後兩條函式實作是可選的,透過間接遞迴定義來描述這兩個函式的關係,這樣只需要提供一個函式的實作就夠了(這種方式稱為 minimal complete definition,最小完整定義)

P.S. GHCi 環境下,可以透過 :info <typeclass> 命令查看該類別定義了哪些函式,以及哪些型別屬於該類別

二.實作

instance 關鍵字用來定義某個 typeclass 的 instance:

instance Eq TrafficLight where
  Red == Red = True
  Green == Green = True
  Yellow == Yellow = True
  _ == _ = False

這裡把 class Eq a 中的型別變數 a 換成了具體的 TrafficLight 型別,並實作了 == 函式(不用同時實作 /= ,因為 Eq 類別中宣告了二者的關係)

試著讓自定義型別成為 Show 類別成員:

data Answer = Yes | No | NoExcuse
instance Show Answer where
  show Yes = "Yes, sir."
  show No = "No, sir."
  show NoExcuse = "No excuse, sir."

試玩一下:

> Yes
Yes, sir.

P.S. GHCi 環境下,可以透過 :info <type> 命令查看該型別屬於哪些 typeclass

子類別

同樣,也有子類別的概念,是指要想成為 B 類別成員,必須先成為 A 類別成員的約束:

class (Eq a) => Num a where
-- ...

要求 Num 類別成員必須先是 Eq 類別成員,從語法上來看只是多了個型別約束。類似的,另一個範例:

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

這裡要求 Maybe a 中的型別變數 a 必須是 Eq 類別的成員,然後, Maybe a 才可以是 Eq 類別的成員

三.Functor

函子(聽起來很厲害),也是一個 typeclass,表示可做映射(能被 map over)的東西

class Functor f where
  fmap :: (a -> b) -> f a -> f b

fmap 接受一個 map a to b 的函式,以及一個 f a 型別的參數,傳回一個 f b 型別的值

看起來有點迷惑, f a 型別是說帶有型別參數的型別,比如 MaybeList 等等,例如:

mapMaybe :: Eq t => (t -> a) -> Maybe t -> Maybe a
mapMaybe f m
  | m == Nothing = Nothing
  | otherwise = Just (f x)
  where (Just x) = m

其中, Maybe t -> Maybe a 就是個 f a -> f b 的例子。試玩一下:

> mapMaybe (> 0) (Just 3)
Just True

map a to b 在這裡指的就是 Maybe NumMaybe Bool

Just 3 :: Num a => Maybe a
Just True :: Maybe Bool

所以, Functor 定義的行為是保留大型別不變( f a ,這裡的 a 是型別變數),允許透過映射( fmap 函式)改變小型別( f a 變到 f b ,這裡的 ab 是具體型別)

帶入 List 的上下文,就是允許對 List 內容做映射,得到另一個 List ,新 List 的內容型別可以發生變化。但無論怎樣, fmap 結果都是 List a (這裡的 a 是型別變數)

聽起來非常自然,因為 List 本就屬於 Functor 類別,並且:

map :: (a -> b) -> [a] -> [b]

這不就是 fmap :: (a -> b) -> f a -> f b 型別定義的一個具體實作嘛,實際上,這個 map 就是那個 fmap

instance Functor [] where
  fmap = map

MaybeList 都屬於 Functor 類別,他們的共同點是什麼?

都像容器。而 fmap 定義的行為恰恰是對容器裡的內容(值)做映射,完了再裝進容器

還有一些特殊的場景,比如 Either

data Either a b = Left a | Right b 	-- Defined in ‘Data.Either’

Either 的型別構造器有兩個型別參數,而 fmap :: (a -> b) -> f a -> f bf 只接受一個參數,所以, Eitherfmap 要求左邊型別固定:

mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a

左邊不做映射,因為映射可能會改變型別,而 Either a (即 fmap :: (a -> b) -> f a -> f bf )是不能變的,所以當 Nothing 一樣處理。例如:

> mapEither show (Right 3)
Right "3"
> mapEither show (Left 3)
Left 3

另一個類似的是 Map

-- 給 Data.Map 起了別名 Map
data Map.Map k a -- ...

Map k v 做映射時, k 不應該變,所以只對值做映射:

mapMap :: Ord k => (t -> a) -> Map.Map k t -> Map.Map k a
mapMap f m = Map.fromList (map (\(k ,v) -> (k, f v)) xs)
  where xs = Map.toList m

例如:

> mapMap (+1) (Map.insert 'a' 2 Map.empty)
fromList [('a',3)]
> mapMap (+1) Map.empty
fromList []

P.S. 這些簡單實作可以透過與標準庫實作做對比來驗證正確性,例如:

> fmap (+1) (Map.insert 'a' 2 Map.empty )
fromList [('a',3)]

P.S. 另外,實作 Functor 時需要遵循一些規則,比如不希望 List 元素順序發生變化,希望二元搜尋樹仍保留其結構性質等等

四.Kind

參與運算的是值(包括函式),而型別是值的屬性,所以值可以按型別分類。透過值攜帶的這個屬性,就能推斷出該值的一些性質。類似的,kind 是型別的型別,算是對型別的分類

GHCi 環境下,可以透過 :kind 命令查看型別的型別,例如:

> :k Int
Int :: *
> :k Maybe
Maybe :: * -> *
> :k Maybe Int
Maybe Int :: *
> :k Either
Either :: * -> * -> *
> :k Either Bool
Either Bool :: * -> *
> :k Either Bool Int
Either Bool Int :: *

Int :: * 表示 Int 是個具體型別, Maybe :: * -> * 表示 Maybe 接受一個具體型別參數,返回一個具體型別,而 Either :: * -> * -> * 表示 Either 接受 2 個具體型別參數,返回一個具體型別,類似於函式呼叫,也有柯里化特性,可以進行部分應用(partially apply)

還有一些更奇怪的 kind,例如:

data Frank a b  = Frank {frankField :: b a} deriving (Show)

對值構造器 Frank 的參數 frankField 限定了型別為 b a ,所以 b* -> *a 是具體型別 * ,那麼 Frank 型別構造器的 kind 為:

Frank :: * -> (* -> *) -> *

其中第一個 * 是參數 a ,中間的 * -> * 是參數 b ,最後的 * 是說返回具體型別。可以這樣填充:

> :t Frank {frankField = Just True}
Frank {frankField = Just True} :: Frank Bool Maybe
> :t Frank {frankField = "hoho"}
Frank {frankField = "hoho"} :: Frank Char []

回過頭來看 EitherFunctor 實作:

> :k Either
Either :: * -> * -> *
> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b

Either 的 kind 是 * -> * -> * (需要兩個具體型別參數),而 fmap 想要的 (a -> b)* -> * (只要一個具體型別參數),所以應該對 Either 部分應用一下,填充一個參數使之成為 * -> * ,那麼 mapEither 的實作就是:

mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a

Either a 就是個標準的 * -> * ,例如:

> :k Either Int
Either Int :: * -> *

P.S. 也可以對著 typeclass 來一發,例如:

> :k Functor
Functor :: (* -> *) -> Constraint
> :k Eq
Eq :: * -> Constraint

其中 Constraint 也是一種 kind,表示必須是某類別的 instance(即型別約束,經常在函式簽名的 => 左邊看到),例如 Num ,具體見 What does has kind 'Constraint' mean in Haskell

評論

暫無評論,快來發表你的看法吧

提交評論