仙境里的Haskell(之四)—— Functor類型類

昨天又有Scala新手在問關於類型參數的問題,Haskell也一樣有不少人一時轉不管那個彎來。 因為類型參數非常重要,所以在今天的內容前再稍微做一些解釋。

所謂類型參數,就是說有個參數,它的類型,也是個參數。。。。

myfst :: (Int, Char) -> Intnmyfst (x,y) = xn n----run-----nmyfst (1,c)n1 ---Intn

上面這個myfst函數,能夠取出(Int,Char)二元組的第一個元素。其實從我們的定義來看,函數實現並不依賴於第一個元素的具體類型,只要是第一個元素,就取出來便是。所以我這個函數應該可以用於(Int,Char),(Char,Int),(String,Int)等,只要是二元組即可。但是同時我又不想丟失它的實際類型,也就是說我希望後續的代碼能夠知道myfst (1,c)的結果是Int類型,myfst(c,1)的結果是Char類型。

所以我們可以用類型參數代替實際類型,要求用戶給出這個類型。

myfst :: (a, b) -> anmyfst (x,y) = xn n----run----nmyfst (c,1)nc ---- Charnnmyfst (1,c)n1 ---Intn

在編譯過程中,編譯器推斷出c的類型是Char,也就是說a在這裡的具體類型是Char,因此函數的返回結果也必須是Char類型。其實完整的寫法應該是

myfst (c :: Char,1)n

但因為Haskell有自動類型推斷能力,所以可以省略類型聲明。

在上一篇里,我們用代數數據類型定義了一個二叉樹(這個定義有點缺陷,但是比較方便,在下文中暫時沿用):

data MyTree a = Leaf a n | Node a (MyTree a) (MyTree a) n deriving (Show)n

並且利用遞歸和模式匹配很方便的定義了獲取最左側葉子節點數據的函數:

leftMost :: MyTree a -> anleftMost (Leaf x) = xnleftMost (Node _ l r) = leftMost ln

那麼假如我們要對MyTree a的每個節點的數據做一個運算,得到另一個MyTree,要怎麼寫呢?也就是:

myMap :: (a -> b) -> MyTree a -> MyTree bn

用Haskell寫代碼的第一件事就是把要設計的函數的類型簽名寫下來,很多時候簽名寫下來,實現的思路也就很清楚了。

myMap :: (a -> b) -> MyTree a -> MyTree bnmyMap f (Leaf x) = Leaf (f x)nmyMap f (Node x l r) = Node (f x) (myMap f l) (myMap f r)n

可以試著運行一下:

myMap (+1) (Leaf 2)nLeaf 3nit :: Num b => MyTree bn nmyMap (+2) (Node 1 (Leaf 2) (Leaf 3))nNode 3 (Leaf 4) (Leaf 5)nnit :: Num b => MyTree bn

具有MyTree a這種性質的數據類型很多,比如

data Option a = Nonen | Some an deriving (Show)n nmyMap :: (a -> b) -> Option a -> Option bnmyMap f None = NonennmyMap f (Some x) = Some (f x)n

用用看:

myMap (+1) (Some 1)nSome 2nit :: Num b => Option bn nmyMap (*2) NonenNonennit :: Num b => Option bn

Option在標準庫里的原型叫Maybe,是一種非常有用的類型,比如我們有個根據email查找User的函數,如果資料庫里並不一定存在給定email所對應的用戶,那麼我們可以把函數的返回類型定義為Maybe User

findUser :: Email -> Maybe Usern

再比如最常用的列表類型,[1,2,3],我們希望對裡面的數據做一個運算得到一個新列表

map :: (a -> b) -> [a] -> [b]n nPrelude> map (*2) [1,2,3]n[2,4,6]n

Haskell是一種能通過類型表達豐富業務含義的強類型編程語言

f1 :: Int -> Int -- 這個運算一定會返回**一個**Int型的結果nf2 :: Int -> Maybe Int -- 這個運算**可能**會返回一個Int或者Nonenf3 :: Int -> [Int] -- 這個運算會返回**不確定個數**個Intn

Maybe代表可能

[]代表不確定個數

對Maybe和[]列表類型來說,他們都支持map運算,這種運算會對它裡面的值做一個運算,結果的性質不變,Maybe仍然是Maybe,[]仍然是[]。

(注意我雖然用裡面的值來表述,但是不要簡單的理解為容器類型,因為容器太狹隘,你很快就會看到不是容器,但也具有相同類型的類型。所以我一般用上下文來表達。)

顯然有一類類型都具有相同的性質,即可以對其所代表的上下文里的數據做個運算,結果仍然處於相同的上下文。如果我們對這類類型做個提煉和抽象,以後看到這種類型就知道怎麼使用了。 前面代碼里出現的三種ADT具有形似的類型定義:

MyTree anOption an[] a -- [a]只是語法糖n

我們可以提煉出類型類Functor(如果用面向對象的習慣,其實類似Mapable的概念):

class MyFunctor f wheren fmap :: (a -> b) -> f a -> f bn

然後我們可以在各自的數據類型里去實現它:

data Option a = Nonen | Some an deriving (Show)nmyMap :: (a -> b) -> Option a -> Option bnmyMap f None = NonennmyMap f (Some x) = Some (f x)nninstance MyFunctor Option wheren fmap = myMapn

相應的,也可以為MyTree提供MyFunctor類型實現

instance MyFunctor MyTree wheren fmap = myMapn

MyFunctor在標準庫的原型就叫Functor,如果看到一個類型是Functor的實例,我們就知道可以對它使用fmap函數。比如說:

Prelude> fmap (*2) (Just 2)nJust 4n nPrelude> fmap (*2) [1,2,3]n[2,4,6]n nnPrelude> let f = fmap (*2) (+1)nPrelude> f 2n6n

且慢!最後一個是什麼鬼呢?


推薦閱讀:

函數式編程在Redux/React中的應用
為什麼諸多編程語言都將模式匹配作為重要構成?
Haskell中的foldl和foldr的聯繫?
紅塵里的Haskell(之一)——Haskell工具鏈科普

TAG:Haskell | 函数式编程 |