簡介
什麼是 Functor ?
現在你可以認為 Functor 是一種資料型別。
Functor 有什麼用 ?
我們可以對 Functor 使用
fmap
。
fmap
是什麼東西 ?
fmap
是一個函式。
fmap
是函式的話,那它的型別簽名是什麼 ?
fmap :: (a -> b) -> f a -> f b
。
我應該怎麼看這個型別簽名 ?
它接受一個函式和一個 Functor 型別作為引數,然後返回另一個 Functor 。
fmap
有什麼用 ?
類似於
map
map (+1) [1,2,3,4,5] -- 返回 [2,3,4,5,6]
fmap (+1) [1,2,3,4,5] -- 返回 [2,3,4,5,6]
所以列表是 Functor ?
是的,List 是 Functor。
為什麼列表是 Functor ?
因為列表實現了
fmap
。instance Functor [] where fmap = map
實現了 fmap
的資料型別都是 Functor ?
不一定。
為什麼 ?
除了要實現
fmap
之外,還需要滿足一些條件才能成為 Functor。
滿足什麼條件 ?
必須保證
fmap id = id
,也就是說fmap id xs
和id xs
必須返回相同的值。必須是可組合的,兩個
fmap
組合使用的結果應該和兩個函式組合起來再用fmap
的結果相同。
也就是說fmap f . fmap g
必須等於fmap (f . g)
。
條件一是什麼意思 ?
意思是
fmap
只能對值呼叫f
,不能做額外的事情。
有具體例子嗎 ?
看看這個經典的自定義資料型別,C表示計數器:
data CMaybe a = CNothing | CJust Int a deriving (Show) instance Functor CMaybe where fmap f CNothing = CNothing fmap f (CJust counter x) = CJust (counter + 1) (f x) -- ghci ghci> fmap (++ "ha") (CJust 0 "ho") CJust 1 "hoha" ghci> fmap (++ "he") (fmap (++ "ha") (CJust 0 "ho")) CJust 2 "hohahe" ghci> fmap (++ "blah") CNothing CNothing
這裡的
fmap
除了對值呼叫f
之外,還對counter
加一。
這有什麼問題嗎 ?
再看看
fmap id
和id
ghci> fmap id (CJust 0 "haha") CJust 1 "haha" ghci> id (Cjust 0 "haha") CJust 0 "haha"
看出問題了嗎 ?
fmap id
和 id
返回的結果不相等 ?
是的,所以即便
CMaybe a
實現了fmap
,但它也不是 Functor。
條件二有點像乘法分配律。
是的。
乘法分配律是(a + b) x c = a x c + b x c
。
而條件二是fmap (f . g) = fmap f . fmap g
。
條件二有具體例子嗎 ?
可以類比函式,因為函式本身也是 Functor,所以函式會滿足可組合這個條件。
而實際應用中,我們也經常使用到函式組合這個特性。
實現了 fmap
,同時滿足兩個條件的資料型別就是 Functor 嗎?
不,還有一個規則,就是該資料型別要有一個型別引數。
能舉個例子嗎 ?
我們已經知道 List 是一個 Functor,先看看 List 的定義:
data [] a = [] | a : [a]
列表有一個型別引數 a,表示一個列表中可以包含相同型別的元素。
Functor 只能有一個型別引數嗎?
不是,我們可以透過其他方法讓多於一個型別引數的資料型別都能成為 Functor 的例項。
什麼手段 ?
你需要先知道怎麼定義一個 Functor。
自定義 Functor
我應該怎麼自定義 Functor ?
先定義一個資料型別,再讓該型別成為 Functor 的例項。
data MyFunctor a = Data a deriving (Show) instance Functor MyFunctor where fmap f (Data x) = Data (f x)
這樣,我們定義的 MyFunctor 就是一個 Functor 了。
剛才提到的讓多於一個型別引數的資料型別成為 Functor 例項的方法是?
利用 Haskell 中不全呼叫的特性。
可以給個例子嗎?
data MyFunctor2 a b = Data2 a b deriving (Show) instance Functor (MyFunctor2 a) where fmap f (Data2 x y) = Data2 x (f y)
在 Haskell 中,我們可以利用 Haskell 不全呼叫的特性,把 MyFunctor2 a 當成一個整體,這樣就相當於只有 b 一個型別引數了。
真 · Functor
我從上面看到,Functor 是一個型別類?
是的。事實上,Functor 是一個型別類,表示滿足一些條件的資料型別。
滿足上面提到的條件?
是的!
有哪些常見的 Functor ?
List
,Maybe
等等。
你可以在 ghci 中輸入:i Functor
來檢視更多預定義的 Functor。
這些 Functor 有什麼特點?
它們都帶有上下文:即可以表示有值,也可以表示空值。
[] 表示空值,[a] 表示有值;
Nothing 表示空值,Just a表示有值;
這樣有什麼好處嗎?
好處是顯然易見的。考慮下下面的虛擬碼:
post = Posts.find_by_id(1) if post return post.title else return None
為什麼這段虛擬碼需要判斷 post 是否為空?因為 post 沒有上下文環境,不能表示空值。
如果 post 有上下文環境 (也就是 post 可以表示空值),那麼我們的程式碼就可以直接寫成:post = Posts.find_by_id(1) return post.title
因此,如果一個值可以帶有上下文環境的話,我們的程式碼就可以寫的非常簡潔。
把剛才的虛擬碼寫成 Haskell 程式碼 ?
fmap (getPostTitle) (findPosts 1)
if else
不見了?
是的,這裡假設 post 是一個 Functor,它可以表示帶有空值的情況。所以
if else
就不需要了。
那 fmap
呢? 它事實上是什麼東西?
fmap
確確實實是一個函式,它知道怎麼把傳進的函式應用到 Functor 中,並返回一個新的 Functor。
fmap
對 Functor 呼叫函式的過程發生了什麼?
看下面兩張圖 (圖出自 Functors, Applicatives, And Monads In Pictures):
實際上,fmap
先取出 Functor 中的值,然後把值傳進函式中,再把函式的返回值放回到 Functor 中,最後返回新的 Functor。
Functor 有什麼限制?
fmap f x
中的f
只接受一個引數。fmap f x
中的f
不能帶有上下文 (換句話說只能是 (+42) 不能是Just (+42)
)。
關於 Functor 的知識,還有什麼我是需要知道的 ?
fmap
可以中綴呼叫,即f `fmap` xs
<$> 是fmap
的別名,一般用於中綴呼叫,即 f <$> xs。
總結
Functor 是型別類,只要滿足以下條件的資料型別都可以成為 Functor 的例項:
實現
fmap
。保證
fmap id = id
。保證
fmap (f . g) = fmap f . fmap g
。該資料型別必須有一個以上的型別引數。
最後,強烈建議看看 這篇文章,相當形象生動。
出處
http://scarletsky.github.io/2016/02/09/what-is-functor-in-haskell/
參考資料
Functors, Applicative Functors and Monoids
Functors, Applicatives, And Monads In Pictures
Functor 簡介
http://stackoverflow.com/questions/2030863/in-functional-programming-what-is-a-functor