我的monad教學
英文版:這裡
這篇文章不探討要怎麼使用monad
如果你想知道怎麼使用它們
你應該先理解它們的實例,如List
, Maybe
, Writer
, State
… 等
那這篇文章要做什麼呢
相信大家在學習monad時
應該都知道它有3個law
可是它們看起來就像是巫術一樣難以理解
我想在讓讀者不必真正理解category theory的狀況下了解那三個law為什麼會是「顯而易見」的
一開始的問題是有好幾種定義monad的方式
它們定義出來的東西都是等價的
但Haskell用的那種是最機掰的
這邊我介紹一種在category theory中相對常見的定義方式
它在Control.Monad
模組裡的宣告如下:
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
為什麼要特別提到這個函數呢?
一般的monad定義是這樣的
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
但存在一種表達力相等的定義方法如下
class Monad m where
return :: a -> m a
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
這兩種不同的monad定義都可以被用來定義對方
我把它當作給讀者的作業
如果用後者的定義來寫出三個law
他們會是這樣:
return >=> f = f
f >=> return = f
(f >=> g) >=> h = f >=> (g >=> h)
雖然很破梗
但以下是monoid的三個law
mempty <> x = x
x <> mempty = x
(x <> y) <> z = x <> (y <> z)
為什麼我要提這個呢?
你仔細一看
會發現這和monad的三個law根本一模模一樣樣啊!
把上面的f
, g
, h
換成a
, b
, c
return
換成mempty
>=>
換成<>
就變成下面了
我把第一個law叫做left identity
意思是在左邊加上「空無」後進行運算的值應該和原本一樣
第二個叫right identity
在右邊加上「空無」後進行運算的值應該和原本一樣
第三個叫結合律
這代表進行運算的順序是不重要的
傳統上加法和乘法的結合律只是它的特例
唯一有點不同的是這點:
對於所有monoid的值a
, b
來說
a <> b
永遠是被定義的
但提到>=>
的參數
那些參數的型別並不只是個像Int
或者是[Bool]
之類的一般型別
而是一個函數型別a -> m b
它們具有某些內部的結構
亦即a
與b
兩者
隨便取兩個a -> m b
和c -> m d
下
>=>
並不總是被定義的
有可能會直接跳type error啊
事實上
唯一不跳type error的狀況是當b ~ c
時
用這樣的方式想
return
和>=>
並不形成一個monoid
而是一個比它更強型別的概念
強型別於第一個參數的回傳型別和第二個參數的接收型別必須有某些相關性
而這樣「強型別的monoid」
在數學上有個名字:category
上面一種monad定義方式的解釋就到此結束
接下來是另一種
數學家更愛可是對入門者來說可能比較難懂的定義
首先
我假定你已經知道什麼是Functor
了
你應該也知道所有Monad
都是Functor
在此
我要把Monad
定義為多滿足某些條件的Functor
用程式碼寫成這樣:
class Functor m => Monad m where
return :: a -> m a
join :: m (m a) -> m a
先來解釋為什麼這種定義可以推導至上面兩種
假設你有a -> f b
和b -> f c
f
僅僅是個Functor
那麼你可以在餵了一個參數給第一個函數後把它丟出的結果拿去用第二個函數map
這麼一來
丟進a
之後
你會得到f (f c)
問題是如果這樣子做了很多詞動作之後f
會越疊越多層
很討厭啊
不能把它全部消去成一層嗎?
如果f
有join
這個方法當然可以
這就是join
在做的事啊!
現在有個巧妙的做法
我們可以只注意型別為* -> *
的Functor
本身
而忽視Functor
之下的型別
為了方便起見
我使用一種奇特的語法
把join
的型別寫作mm -> m
他把靠在一起(事實上是互相嵌套)的兩個同樣的Monad
合併成一個
那return
的型別要怎麼用這種奇特的語法表示呢?
你應該知道有個什麼都不做的Functor
叫Identity
a
的特性和Identity a
一模一樣
畢竟Identity
只是個空殼子而已
我在此把它簡寫成I
return
的型別相等於I a -> m a
用奇特的語法寫就是I -> m
接下來可以來再次介紹Monad
的定義了
Monad
能被定義為一個有著return
和join
動作的Functor
它們同樣要滿足三個law
分別如下
-
我們可以把一個
m
的左邊加上一個I
(事實上是外面套一層I
)
之後把這個I
用return
變成m
怎麼變的?
用奇怪語法的方式想
return
是I -> m
所以它本來就是用來把I
變成m
的啊!
現在我們有兩層m
了
也就是mm
接著我能用join
(mm -> m
)變回一層
多加一層再變回一層這樣的動作應該和什麼都不做一樣 -
我可以把
m
的右邊加上一個I
不用奇怪語法的角度想
我只要把m
裡面的型別a
map
成I a
就好了
用奇怪語法的角度想就是在右邊加上I
現在我可以同樣用return
把mI
變成mm
然後再join
成m
同樣這樣子的動作應該和什麼都不做一樣 -
假如有三層
m
也就是mmm
我可以join
兩次把它變成m
有兩種方式
第一種方式是先join
左邊兩個m
第二種是先join
右邊兩個m
但順序應該不影響結果
但這就是Monoid
的三個law啊!
第一個就和left identity一樣
左邊加上空無應該不讓結果改變
第二個就是right identity
右邊也能加上空無
第三個就是結合律
運算的順序不影響結果
這難道還不夠理所當然嗎?
也就是說
A monad is just a monoid in the category of endofunctors, what’s the problem?