原文地址: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
|
|
函子的類型類只定義了一個fmap函數: fmap函數接收兩個參數,第一個參數是以a爲參數,b爲返回值的函數;第二個參數類型爲f a,fmap的返回值類型爲f b. 注意這裏的a, b可以爲任意類型, f爲接收一個類型參數的類型構造器。這樣說可能有點抽象,來看一個具體的例子。 已知[](列表)是一個functor實例,他的fmap函數聲明爲:
1
|
|
接收一個以a爲參數,b爲返回值的函數以及元素類型爲a的列表,返回元素類型爲b的列表。 至此,你能看出functor所抽象的行爲嗎?你可以從下面兩個角度思考fmap: 1. 接受函數和函子值,返回在函子值上映射函數的結果(返回也是函子值)。 2. 接受函數,把該函數從操作普通類型的函數提升(lift)爲操作函子值的函數。 這就是函子,不難吧?
2. Applicative
Applicative,俗稱加強版函子,先來看Applicative類型類的代碼:
1
2
3
|
|
多了一個新的元素,先解釋下。 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
|
|
pure的實現很簡單,把接收的參數值放入列表並返回。<*>
的實現稍微複雜點,使用了列表生成式的語法。如果你接觸過Python,對這種語法不會陌生,
這段代碼如果用命令式語言風格翻譯,會是這樣:
1
2
3
4
5
6
|
|
到此爲止,我們應該已經瞭解Applicative實例的作用了,主要定義了兩個行爲,第一個行爲是接收一個任意值爲參數,返回一個函子值, 第二個行爲是從一個函子值裏取出函數,應用到第二個函子裏面的值。那麼Applicative有什麼實際用處呢,看下面的應用:
1
2
|
|
(*0)
是對*
函數的部分應用,*
是一個二元函數,接收兩個參數,返回這兩個參數的乘積。(*0)
是一個一元函數,接收一個參數,
返回這個參數與0的乘積。第一個列表裏面是三個一元函數,分別應用到第二個列表的元素,參照我們之前對列表<*>
函數的定義,很容易得出上面的結果。
這種把<*>
串起來用的用法叫做Applicative風格,下面是另外幾個例子:
1
2
3
4
|
|
非常好,現在我們能夠把應用到普通值的函數應用到函子值上面了。
3. Monad
相比Functor和Applicative,Monad的應用更加廣泛,Monad可以看作加強版的Applicative,引用<<Haskell趣學指南>>中的句子:
1
2
|
|
這種場景非常常見,我們來看Java8的Stream API:
1
2
3
4
5
|
|
flatMap方法接收一個函數(這個函數接收T類型的入參,返回Stream)然後在Stream上應用這個函數,返回Stream(暫時不考慮子類問題)。 我們來看看Haskell中的"flatMap"(Haskell中叫做綁定):
1
|
|
>>=
函數接收兩個參數:一個Monad值m
a,一個函數(入參爲類型a,返回值爲Monad值m b),返回值類型爲Monad值m b。 這和Java8的flatMap形式上非常相似,其實它們要解決的是相同的問題。 Monad在函數式編程中無處不在,來看看具體的例子。
1
2
|
|
首先,列表是一個Monad,對照>>=
函數的定義,這裏的m就是列表,a就是Int,a
-> m b對應的類型就是Int -> [Int]。 上面的邏輯用Java8的flatMap實現:
1
2
|
|
好了,這裏我簡單介紹了Functor,Applicative,Monad的概念以及實際應用。如果對這些概念想更進一步瞭解,可以看看下面的書:
1. Haskell趣學指南
2. Functional
Programming in Scala
3. 範疇論