延著走道往下,我走過一個又一個的房間。忽然有一種奇怪的念頭浮上,我感覺其實每個房間裡的鏡子雖然看似迥異,但其實一直都是同一面鏡子。它只是跟隨著我,不斷的移動到下個房間裡。而這面鏡子會依它所在的地方,變換其樣貌…
看似走到建築物的底端,但等在走道盡頭的,卻是一座樓梯。
居然還要去樓上…
這個嘛…看情況。Maybe 也可以是一種 Monoid,但是有條件。那個條件就是當這個 Maybe 裡裝的東西,是一個 Semigroup 的時候,那麼就可以推導出這個 Maybe 的 Monoid 性質。
-- Haskell 語法
-- Maybe Monoid 的型別宣告
instance Semigroup a => Monoid (Maybe a)
如果兩個 Maybe Monoid <>
在一起的時,兩邊都是有值的 Just
,那麼就會用裡面的值(記得它是 Semigroup 嗎?) 的 <>
的方式併在一起,再裝回 Just
裡。
而如果任何一邊是 Nothing
時,那就忽略 Nothing。
-- Haskell 語法
Just (Sum 1) <> Just (Sum 2) -- => Just (Sum {getSum = 3})
Just (Sum 1) <> Nothing -- => Just (Sum {getSum = 1})
不過當我們說到忽略 Nothing 這件事的時候,請記得這是 Haskell 在 Maybe Monoid 上實作的行為。Maybe 的 Monoid 有其它可能,只是沒有實作出來。而這個可能,我們會在其它 typeclass 上看到。
要讓元組是 Monoid,也有它的條件。跟 Maybe 的情況類似,元組要成為 Monoid,那麼它裡面的成員也都要是 Monoid 才行:
-- Haskell 語法
-- 元組們的 Monoid 的型別宣告
instance (Monoid a, Monoid b, Monoid c, Monoid d, Monoid e) =>
Monoid (a, b, c, d, e)
instance (Monoid a, Monoid b, Monoid c, Monoid d) =>
Monoid (a, b, c, d)
instance (Monoid a, Monoid b, Monoid c) => Monoid (a, b, c)
instance (Monoid a, Monoid b) => Monoid (a, b)
-- 試試看
(Sum 1, Product 2) <> (Sum 2, Product 3)
-- => (Sum {getSum = 3},Product {getProduct = 6})
在這裡讓你停下來想一下。
其實跟整數的情況一樣,可以運用在布林值上,符合封閉律與結合律的二元運算也不只一個。所以就像整數有 Sum
及 Product
一樣。布林值也有 All
及 Any
這兩個型別:
-- Haskell 語法
(All True) <> (All False) <> (All True) -- => All {getAll = False}
(Any False) <> (Any False) <> (Any True) -- =>Any {getAny = True}
順帶一提,還有 First
及 Last
這兩個可以做用在 Maybe
上的 Monoid 也蠻有趣的。
那麼,我們要來問一個很重要的問題了: 函式,也是一種 Monoid 嗎?
我們可以先試著證明函式是不是一種 Semigroup。方法就是找到一個二元運算(或幾個函式的組合成的二元運算),可以符合 1. 封閉律 及 2. 結合律。
想到了嗎?接收兩個函式,回傳一個函式的東西………
是的。 .
。
函式的組合就是一種符合封閉律與結合律的二元運算。我們可以證明一下:
-- Haskell 語法
f = (+ 1)
g = (* 2)
h = (+ 3)
cf1 = (f . g) . h
cf2 = f . (g . h)
cf1 1 == cf2 1
那麼看來似乎我們只要找到一個跟任何函式 .
組合在一起時,能讓該函式依然是函式自己的函式就好了 (這句有點難唸,希望你腦袋裡的聲音沒有咬到舌頭。)
得益於 Haskell 函式的天生懶惰特性,我們至今都還沒在 Haskell 裡用過匿名函式。但是 Haskell 裡其實是有匿名函式的。它的語法如下:
-- Haskell 語法
\x -> x + 1
參數的前方要加一個反斜線 \
,據聞是因為這看起來跟 很像,只是少了左邊的腳而已(哈哈)。當然,它也完全符合一等公民的所有性質,可以指派給變數,當做呼叫其它函式的引數……你知道的。
那個跟任何函式組合,而不會造成任何改變的函式,長得像這樣:
-- Haskell 語法
\x -> x
接收到一個參數,並原封不動的回傳的函式,我們稱之為恆等函式。而這個函式雖然看起來什麼事都沒做,但其實它有許多有趣而且非它不可的用法。因此 Haskell 裡其實就內建了這個函式,叫做 id
。
-- Haskell 語法
id 1 -- => 1
id True -- => True
當我們知道 id
這個函式,就是函式的 mempty
之後,要證明函式也是 Monoid,我們還少考慮了一件事,那就是當兩個函式合併之後,這兩個函式的回傳值要怎麼合併在一起?想一下吧。
就跟上面的幾個型別一樣,函式要成為 Monoid 的條件,就是這兩個函式的回傳值,也要是同型別的 Monoid 才行:
-- Haskell 語法
-- 內建:函式 Monoid 的實作
instance Monoid b => Monoid (a -> b) where
mempty _ = mempty
mappend f g x = mappend (f x) (g x)
-- 用用看!
f x = Sum x * 10
g x = Sum x - 1
h = f <> g
h 20 -- => Sum {getSum = 219}
那麼,我們是不是可以在串列裡,放進一堆函式,只要這些函式的回傳值是同類型的 Monoid,那麼就可以把它們用 fold
(也就是 reduce
) 組合成一個函式了?
終於我看到了一個鑲嵌著大塊板子的平台,而這次沒有東西擋在上面了。仔細看了一下,上面寫著……
[to be continue]