Monad
對工程師而言,如果沒牽扯到數學相關的範疇其實不是那麼可怕難懂的東西。
|
|
Functor
實作了fmap
,使得可將普通函數(a -> b)
lift到f a -> f b
;
|
|
Applicative
實作了<*>
,讓我們可以將保裹在某 structure 中的函數f (a -> b)
,apply 到同樣含有相同結構的數值中f a -> f b
他們做的事情其實都是 function application,只是應用的情境不同而已。而從工程面來看,Monad 其實也是一樣的,也為我們做了某情境下的函數調用的抽象。
Monad TypeClass
Monad typeClass 的部分定義如下
其中可以看到 Monad 必須是 Applicative,所以return
的預設定義就是pure
,將一個數值包進 monadic structure m
。
>>=
(bind),是使用monad進行操作時十分常看到的東西。
從他的 type signature 不難發現,他要做的事也是 function application,只是這次要應用的函數是(a -> m b)
,而這函數會所 return 的值是將 input 包上一個額外的結構m
,而最終>>=
所返回值m b
會跟輸入m a
是具有相同結構的。
Monad laws
如同Applicative Functor
,就算一個型別實作了>>=
成為了 Monad 的instance了,也還不能夠說是一個Monad
。
必須還要在滿足三個 Monad laws
這部分可以自行拿一些已知的Monad去驗證(List, Maybe, Either Monad),而這在自行定義 Monad 時也是要自己去確保滿足的,Haskell compiler 並不會跟你說符不符合。
Maybe Monad
當在做除法運算的時候,會不希望除數為 0,否則後續運算可能會出現非預期的結果,因此定義了一個safeDivide
safeDivide
會回傳Maybe Double
,這樣子的話做其他的運算或者說在做函數組合時,其他函數 input 型別都也要改成Maybe
型別,才可以拿其結果作為輸入來使用。
所以在其他函數內部,都需根據 Maybe 的兩種可能性Just a
與Nothing
做不同的運算。
|
|
可以想像,每個 maybeFunc 都要去做這些判斷是會非常麻煩,要寫很多重複的程式碼。
Maybe Monad 就可以幫我們將判斷這些case ... of
的工作抽象出來
|
|
其>>=
的定義,幫我們做掉判斷Nothing
跟Just
然後做不同任務的工作。
當是 Nothing 時,就不理會後續的函數k
,直接返回 Nothing。
若是 Just 時,則將其中的值a
,傳進後續要執行的函數k
中。
也可以看到,以Maybe
而言,使用>>=
我們就可以不斷串接有這種a -> Maybe b
型別的函式,因為(Just x) >>= k
回傳的也還是Maybe
型別的數值,可以繼續用>>=
串接下一步的運算。
事實上就是,如果我們有一系列的函數a -> m b
,我們可以藉由>>=
將這些相依的運算做序列組合。
所以原本的函數組合就很容易可以這樣寫
同樣類似的行為,如Either Monad
也能幫助我們省下判斷Left
和Right
的程式碼
List Monad
首先看一下如何定義 List Monad 內容
return
和其在Applicative
中定義的pure
一樣,就是把值丟進一個 List 中。>>=
的行為是將f
map 到 List 中,然後concat最終結果。
|
|
其實上面 List Monad 的bind>>=
所做的事情,可以用List comprehension達到。
而實際上,原始碼中,List Monad 的>>=
就是用 List comprehensions 實作的。
|
|
所以從List Monad來看,因為兩者行為的等價,也比較能明白為何有人會說 Monad 做的事情,其實就是 flatmap 或 concatmap。
Do notation
如果只用>>=
的話有可能會發生程式碼巢狀結構太深的問題
這時候可以使用do notation這個語法糖,來幫助用imperative programming的形式由上至下撰寫代碼,增加可讀性
Type signature
這段希望能從type signature來得到一些操作上的直覺
Function Application
從 TypeClass 知道,若一個東西是 Monad,那他必然是 Applicative 亦即也必然是 Functor。
所以Functor、Applicative、Monad在應用的行為上肯定有一定程度的一致性。
三者從 type signature 來比較,更有一開始所說的,都是將 function application 做不同應用情境的抽象。
Dependent computation
同樣從>>=
的型別去看
>>=
接受了m a
,而第二個參數是個函數(a -> m b)
,其中的a
從哪來,就是從m a
的運算結果而來。
因此>>=
所做的事情,就是將一個 monadic structure 所包裹的 computation m a
,傳遞到串接的函數中 (a -> m b)
,讓此函數根據傳遞進來的參數去做某些運算,最後再返回m b
。而此m b
又可搭配其他 monadic function (a -> m b)
,繼續根據函數返回值操作下去。
而這種相依關係也是 Monad 較靈活的原因之一,因為它可以根據函數返回的值再去後續的運算。而這是 Applicative 無法做到的,<*>
所串接的函數彼此是不相依的。
General concatenation
如果都是去 lift 普通函數並且 apply 到有額外結構的值中,如f a
, m a
,那麼用 fmap 就行了。
但如果這個函數a -> m b
,是返回一個有 monadic structure 的值時,使用fmap會發生什麼事情呢?
可以看到最終返回的結果是m (m b)
,但我們並不想要有改變原有的架構,變成巢狀的結構,如巢狀的 List。所以要想辦法將 m (m b)
轉換成 m b
,也就是去 flattern 或是 concate 這個雙層的結構。
而這就是 Monad 的一個特色所在,join
。
可以看到,join
所做的事情就是去concate這兩層m m
為一個m
。因此也可以看出 Monad 其實提供了更 general 的 concat 方法。
所以我們用join
和fmap
其實就可以構造出>>=
|
|
因此可以看出,其實 Monad 就是提供一個方法,讓我們去 map 函數,最後再將其結果join起來。
結語
Monoid
, Funtor
, Applicative
, Monad
… 這些 typeclass,都代表著某些行為的抽象。
所以這些東西真的有存在的必要嗎?個人認為就工程的角度而言都沒有。
今天就算把名稱換成Apple
, Orange
, Banana
, Pineapple
也是可以(並且比較不會讓人畏懼?),叫什麼名字都不是重點。並不是因為他是Monad
我們才可以做到什麼什麼,一樣可以用其他方式來達成一樣的目的。
我們需要是程式行為上的抽象或是本質上的改變,使得程式碼的重用性和表達能力能夠提升,而這也正是 functional programming language 的強項。
而因為這些被抽象的行為產生出來的輪廓 (type signature),可以跟範疇論 (category theory) 去結合,所以拿範疇論的專有名詞作為 typeclass 的名稱,較能將數學與程式結合的意圖反應出來。
除此之外我們可以看到,不同型別(Maybe, List, …),在不同 typeclass instance 的定義中,儘管都有做fmap
,<*>
, >>=
等等的函數存在,但他們所做的事情、邏輯是獨立的,但最終他們所具有的結構是一致的。
我們其實也可以將其命名為listFmap
, maybeFmap
… 等等,但因為統一了 interface 大家的名稱保持一致,並且具有相同的結構,這帶來的好處就是能再更近一步的去重用這些代碼,建構更高層次的抽象方法。
參考資料
What I Wish I Knew When Learning Haskell 2.3 ( Stephen Diehl )