梅賈德斯不是照人類傳統的時間來記戴,而是著眼在一個世紀發生的生活故事,一切同時存在於一瞬間。
-- 加布列·賈西亞·馬奎斯, 百年孤寂
又一次來到牆的前面。即使它還是半透明的,但卻依然能感覺到那個悠久而質樸的感覺。慢慢延著牆散步,看著其上各式各樣的痕跡,可以感受到有多少想穿過它的嘗試。
但那座牆上是有個門的。而我現在看得到了。
我朝著門走去,而那本書也跟了上來:
Monad 的實作:
class Applicative m => Monad (m :: * -> *) where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a {-# MINIMAL (>>=) #-}
Monad 的法則:
- 單位元素
- 結合律
在談 Monad 是什麼之前,我認為更加重要的,是先確定一下 Monad 不是什麼:
在參悟的過程中,覺得有點懷疑的時候,可以回頭來對照一下這張表。
fmap
的函式也是回傳一個串列時讓我們回到串列那個大家都愛用的 map
來看一下:
-- Haskell 語法
f = (+1)
map f [1, 2, 3] -- => [2, 3, 4]
那麼如果我們有個函式 g
,輸入一個數字,就會回傳一個串列的話:
-- Haskell 語法
g x = [x, x + 1, x + 2]
g 10 -- => [10, 11, 12]
那麼我們用 g
這個函式,對一個串列進行 map
時,會回傳……串列包著的串列:
-- Haskell 語法
map g [1, 2, 3] -- => [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
很多時候,我們會希望上面那個串列的串列,是能自動坍縮成一層的陣列的。如果你很習慣用其它語言裡的 map
,那麼你八成也知道有一個這種特性的函式: flat_map
:
# Elixir 語法
g = fn x -> [x, x + 1, x + 2] end
Enum.flat_map([1, 2, 3], g)
# => [1, 2, 3, 2, 3, 4, 3, 4, 5]
而在 Haskell 裡,你要用 concat
這個把串列的串列攤平的函式,與 map
一起來做到這樣的事:
-- Haskell 語法
concat [[1, 2], [3, 4]] -- => [1, 2, 3, 4]
concat $ map (\x -> [x, x + 1, x + 2]) [1, 2, 3]
-- => [1,2,3,2,3,4,3,4,5]
嗯。這個就是 Monad 的特性。
return
?許多人在看到型別定義裡,那個叫 return
的函式時,通常會非常疑惑。因為絕大部份的程式語言裡,都把這個字當做是終止目前的計算,並回傳結果用的關鍵字。但是仔細看一下,在 Monad 裡,這個 return
只是一個非常普通的函式。而它的型別是:
return :: a -> m a
仔細看一下,它跟 Applicative 裡的 pure
基本上是同一個東西。它不會終止什麼計算,也沒有什麼特殊的行為。就是個單純的拿到一個值,把值裝進容器裡的函式而己。
但是之所以選這個字,是有其用意的,之後我們就會看到了。
當然 Monad 重點並不在於把嵌疊的串列坍成一層而己。如果談論的對象只是串列的話,那就用 flat_map
,或 concat $ map
來理解就可以了。而 Monad,既然是一個 typeclass ,那麼理解的重心,則是放在坍縮成一層這個概念,以及有哪些容器也能有這個坍縮成一層的特性,接著會延伸出這個特性的不同使用手法。
讓我們先來看 Monad 最重要的 >>=
函式,大家稱它為 bind
。這個函式先接收一個 Monad f a
,再接收一個函式 (a -> fb)
,最後會回傳包在單層容器裡的 f (b)
。
為了要看這一系列 typeclass 的函式型別變換,我們把 >>=
改成用方向相反的 =<<
來表示,這個反過來的 =<<
則是一個先接收函式,再接收 Monad 的函式。
另外我們依序列出從函式應用 $
、Functor 的 <$>
(fmap 的中綴形式)、Applicative 的 <*>
與 Monad 的 =<<
的型別,一個個往下排,並加上空白看看:
-- Haskell 語法
$ :: (a -> b) -> a -> b
<$> :: Functor f => (a -> b) -> f a -> f b
<*> :: Applicative f => f (a -> b) -> f a -> f b
(=<<) :: Monad f => (a -> f b) -> f a -> f b
--- 參考用: 原本的 >>= 的型別
(>>=) :: Monad f => f a -> (a -> f b) -> f b
我們之前已經看過,從 $
到 <$>
,第一個參數的函式保持不變,而接收的第二個參數,從處理單純的值,變成可以處理裝在容器裡的值,是所謂的升格。
而從 <$>
到 <*>
,第一個參數的函式,也被放到容器裡。而我們也說過,其行為可以看做容器裡的兩個值(一個函式,另一個是引數),用函式呼叫產生結果,而兩邊的容器外殼,則會像是 Monoid 那樣結合 (mappend
) 在一起。
而當我們談到 =<<
,如果只是單純的用 fmap
/<$>
將第一個參數的 (a -> f b)
的函式,應用到f a
裡的話(注意 a
被裝在 f
容器裡)。那麼其結果的型別會是:f (f b)
(想一下兩層的串列)。而 =<<
的特性,就是它會把這個兩層相同的 f
容器坍縮成一層,讓結果是 f b
。
是的。 Monad 的本質,就是這樣而己。
當我正想再去找萬用鑰匙時,發現這次門上面的標識不太一樣。開孔上方,寫著這樣的字:
join :: m (m a) -> m a
所以是想要一個可以把雙層的容器坍縮成一層容器的函式囉?我仔細想了一下……
[to be continue]
是說,這個故事是有結局的,但原本就預計寫在下一篇而且還在趕工中。那麼原則上結局跟感言會明天或後天再貼上來 XD