What is functor in Haskell ?

scarlex發表於2016-02-11

簡介

什麼是 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。

滿足什麼條件 ?

  1. 必須保證 fmap id = id,也就是說 fmap id xsid xs 必須返回相同的值。

  2. 必須是可組合的,兩個 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 idid

ghci> fmap id (CJust 0 "haha")
CJust 1 "haha"
ghci> id (Cjust 0 "haha")
CJust 0 "haha"

看出問題了嗎 ?

fmap idid 返回的結果不相等 ?

是的,所以即便 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_just
fmap_nothing
實際上,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

相關文章