iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 30
1
Software Development

mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉系列 第 33

mostly:functional 第二十九章:Monad 的法則

梅賈德斯不是照人類傳統的時間來記戴,而是著眼在一個世紀發生的生活故事,一切同時存在於一瞬間。

-- 加布列·賈西亞·馬奎斯, 百年孤寂


又一次來到牆的前面。即使它還是半透明的,但卻依然能感覺到那個悠久而質樸的感覺。慢慢延著牆散步,看著其上各式各樣的痕跡,可以感受到有多少想穿過它的嘗試。

但那座牆上是有個門的。而我現在看得到了。

我朝著門走去,而那本書也跟了上來:

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 的法則:

  1. 單位元素
  2. 結合律



Monad 不是些什麼

在談 Monad 是什麼之前,我認為更加重要的,是先確定一下 Monad 不是什麼

  • 不是副作用
  • 不是 Haskell 裡執行指令式程式設計 (imperative programming) 的專門語法。
  • 不是一個值
  • 不是積極執行 (strictness)
  • 不是設計模式

在參悟的過程中,覺得有點懷疑的時候,可以回頭來對照一下這張表。

當用來 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:不只能坍平串列

當然 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]


上一篇
mostly:functional 第二十八章的試煉: Applicative 的證明
下一篇
mostly:functional 終章:Monad 的實體
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

1 則留言

0
taiansu
iT邦新手 5 級 ‧ 2020-10-14 00:06:05

是說,這個故事是有結局的,但原本就預計寫在下一篇而且還在趕工中。那麼原則上結局跟感言會明天或後天再貼上來 XD

我要留言

立即登入留言