簡單介紹函數式編程中的Functor(函子),Applicative(加強版函子),Monad(單子)

原文地址:http://skaka.me/blog/2015/12/19/functor-applicative-monad-scala-haskell/

如果你是剛接觸函數式編程,可能很容易被下面這些術語弄迷惑:Functor(函子),Applicative(加強版函子),Monad(單子)。 這些概念不是空穴來風,它們出自範疇論,如果你上網去搜範疇論,可能會找到大篇的術語定義,學術資料,這些資料大多都不是入門友好的。 這裏我不會探討定義,只會介紹這些概念在代碼中到底起了什麼樣的作用,以及怎麼樣運用它們。

下面的示例代碼大部分是Haskell,有一小部分是Java8,不會Haskell完全沒關係,你可以把它們看作僞代碼,我會對每一段代碼進行解釋。 這篇文章適合剛剛接觸函數式編程的同學。我在剛接觸這些概念的時候一頭霧水,網上找的資料要麼level太高看不懂, 要麼直接就blabla給你介紹一大片背景知識了。後來經過長時間的摸爬滾打加實踐,我發現這些概念理解起來也不是很困難,所以就想寫一篇入門級的介紹。 如果你想要對函數式編程有一定的瞭解,這些概念你是繞不過去的,特別是Monad,當你發現你理解了Monad的機制,很多看起來不可思議的代碼就能理解了。

開始之前,我簡單介紹一下類型類(typeclass)和類型構造器的概念。函數式編程中的類型類是定義行爲的接口。 如果一個類型是某個類型類的實例,那麼這個類型必須實現所有該類型類所定義的行爲。不要因爲有“類”這個詞就把類型類與面向對象中的類混淆, 他們是完全不同的概念。類型構造器能夠接收其他類型爲參數,創建出新的類型。舉個例子,Scala的List即爲接收一個類型參數的類型構造器, 當類型參數爲Int時,List類型構造器的返回類型爲List[Int],當類型參數爲String時,返回類型爲List[String]。 與類型構造器相對的概念是值構造器,比如Int(2)。

1. Functor

首先看看函子的類型類用代碼怎麼表示:

1
2
3
-- Haskell中一個函數如果有兩個參數一個返回值,寫法是這樣: a(第一個參數) -> b(第二個參數) -> c(返回值)
class Functor f where
  fmap :: (a -> b) -> f a -> f b

函子的類型類只定義了一個fmap函數: fmap函數接收兩個參數,第一個參數是以a爲參數,b爲返回值的函數;第二個參數類型爲f a,fmap的返回值類型爲f b. 注意這裏的a, b可以爲任意類型, f爲接收一個類型參數的類型構造器。這樣說可能有點抽象,來看一個具體的例子。 已知[](列表)是一個functor實例,他的fmap函數聲明爲:

1
fmap :: (a -> b) -> [a] -> [b]

接收一個以a爲參數,b爲返回值的函數以及元素類型爲a的列表,返回元素類型爲b的列表。 至此,你能看出functor所抽象的行爲嗎?你可以從下面兩個角度思考fmap: 1. 接受函數和函子值,返回在函子值上映射函數的結果(返回也是函子值)。 2. 接受函數,把該函數從操作普通類型的函數提升(lift)爲操作函子值的函數。 這就是函子,不難吧?

2. Applicative

Applicative,俗稱加強版函子,先來看Applicative類型類的代碼:

1
2
3
class (Functor f) => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

多了一個新的元素,先解釋下。 class (Functor f) => Applicative f的意思是約束f類型必須首先是一個Functor(函子),即如果一個類型是Applicative的實例, 則肯定是Functor的實例。Applicative類型類定義了兩個函數:pure和<*>(其實還有一個fmap, 因爲Applicative實例肯定是Functor的實例,所以fmap免費提供了)。pure是一個很簡單的函數,接收任意類型的值爲參數,返回包裹了該值的Applicative值。 <*>函數看起來和fmap有些像,唯一的區別是fmap的第一個參數接收一個普通函數(a -> b),而<*>的第一個參數爲f(a -> b), 即把普通的函數用Applicative包裹。我們看看列表作爲Applicative實例的實現:

1
2
3
-- Haskell中,列表的寫法爲[], 例如[1,2,3]是一個元素都爲Int的列表
pure x = [x]
(<*>) fs xs = [f x | f <- fs, x <- xs]

pure的實現很簡單,把接收的參數值放入列表並返回。<*>的實現稍微複雜點,使用了列表生成式的語法。如果你接觸過Python,對這種語法不會陌生, 這段代碼如果用命令式語言風格翻譯,會是這樣:

1
2
3
4
5
6
var array = [];
for(f in fs) {
  for(x in xs) {
    array.push(f(x));
  }
}

到此爲止,我們應該已經瞭解Applicative實例的作用了,主要定義了兩個行爲,第一個行爲是接收一個任意值爲參數,返回一個函子值, 第二個行爲是從一個函子值裏取出函數,應用到第二個函子裏面的值。那麼Applicative有什麼實際用處呢,看下面的應用:

1
2
[(*0), (+100), (+200)] <*> [1, 2, 3]
-- 輸出結果: 0,0,0,101,102,103,201,202,203

(*0)是對*函數的部分應用,*是一個二元函數,接收兩個參數,返回這兩個參數的乘積。(*0)是一個一元函數,接收一個參數, 返回這個參數與0的乘積。第一個列表裏面是三個一元函數,分別應用到第二個列表的元素,參照我們之前對列表<*>函數的定義,很容易得出上面的結果。 這種把<*>串起來用的用法叫做Applicative風格,下面是另外幾個例子:

1
2
3
4
[(+), (*)] <*> [1, 2] <*> [3, 4]
-- 輸出結果: [4,5,5,6,3,4,6,8]
pure (+) <*> [1, 2] <*> [3, 4]
-- 輸出結果: [4,5,5,6]

非常好,現在我們能夠把應用到普通值的函數應用到函子值上面了。

3. Monad

相比Functor和Applicative,Monad的應用更加廣泛,Monad可以看作加強版的Applicative,引用<<Haskell趣學指南>>中的句子:

1
2
monad是對applicative函子概念的延伸,它們提供了下面這個問題的一種解決方案:如果我們有一個帶有上下文的值m a,如何對它應用這樣一個函數——取
類型爲a的參數,返回帶上下文的值?換句話說,怎麼對m a應用類型爲a -> m b的函數

這種場景非常常見,我們來看Java8的Stream API:

1
2
3
4
5
public interface Stream<T> extends BaseStream<T, Stream<T>> {
  //...
  <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
  //...
}

flatMap方法接收一個函數(這個函數接收T類型的入參,返回Stream)然後在Stream上應用這個函數,返回Stream(暫時不考慮子類問題)。 我們來看看Haskell中的"flatMap"(Haskell中叫做綁定):

1
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

>>=函數接收兩個參數:一個Monad值m a,一個函數(入參爲類型a,返回值爲Monad值m b),返回值類型爲Monad值m b。 這和Java8的flatMap形式上非常相似,其實它們要解決的是相同的問題。 Monad在函數式編程中無處不在,來看看具體的例子。

1
2
[3,4,5] >>= \x -> [-x]
-- 輸出結果: [-3,-4,-5]

首先,列表是一個Monad,對照>>=函數的定義,這裏的m就是列表,a就是Int,a -> m b對應的類型就是Int -> [Int]。 上面的邏輯用Java8的flatMap實現:

1
2
//Lists來自Guava庫
Lists.newArrayList(3,4,5).stream().flatMap(x -> Lists.newArrayList(-x).stream()).collect(Collectors.toList());

好了,這裏我簡單介紹了Functor,Applicative,Monad的概念以及實際應用。如果對這些概念想更進一步瞭解,可以看看下面的書:
1. Haskell趣學指南
2. Functional Programming in Scala
3. 範疇論


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章