iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
1
Software Development

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

mostly:functional 第二十二章:Monoid 的實體

來到隔壁的建築,我發現這裡幾乎跟剛才那棟非常類似,房間的格局、配置、擺設。但不同的是,這裡的每一間房間,都有一面鏡子。




中綴與前綴

我們可以發現,Semigroup 的 <> 跟 Monoid 的 mappend 型別一樣。事實上,這兩個是同樣的東西。

-- Haskell 語法
class Semigroup a where
  (<>) :: a -> a -> a
  ...
infixr 6 <>
-----------
class Semigroup a => Monoid a where
  ...
  mappend :: a -> a -> a
  ...


-- 試試看
[1, 2, 3] <> [4, 5, 6]      -- => [1, 2, 3, 4, 5, 6]

mappend [1, 2, 3] [4, 5, 6] -- => [1, 2, 3, 4, 5, 6]

在定義裡,我們可以看到在 <> 的底下有寫著 infixr,表示這是一種右結合中綴函式。中綴函式,顧名思義,就是寫在中間的。例如我們常用的 +-*/,都算是中綴函式,像是這樣 1 + 2

而與中綴函式不同的,則是前綴函式,用法則是放在前面的,例如 add 1 2。在大多數的程式語言的慣例裡,用英文字母定義的,大多是前綴函式。而用符號定義的接受兩個參數的函式,也就是二元函式,大多是中綴的。

在這個例子裡,mappand<> 的功用一模一樣,只是前綴跟中綴的區別而己。

而右結合,代表的是如果有 Sum 1 <> Sum 2 <> Sum 3 的程式碼,那麼在沒有括號的情況下,會先加右邊兩個,也就是 Sum 2Sum 3,最後再與左邊的 Sum 1 相加。

而定義裡中間那個 6,則是這個中綴函式的與其它函式一起使用時的優先度 (priority),數字愈高愈早進行處理。例如乘法 * 的優先度是 7,而加法 + 的優先度是 6。

前綴轉中綴,以及反過來

在 Haskell 中,你可以把一個中綴函式當做前綴使用。方法是在它的兩側加上圓括號 ()

-- Haskell 語法
(<>) (Sum 1) (Sum 2) -- => Sum {getSum = 3}

而當然也可以把二元的前綴函式當做中綴使用,用法則是在前綴函式的兩側加上 backtick:```

-- Haskell 語法
Product 2 `mappend` Product 3 -- => Product {getProduct = 6}

什麼東西是 Monoid

而在我們確定了 Monoid 的法則之後,我們來思考一下有哪些東西可以算是 Monoid:

字串是一種 Monoid 嗎?

字串是一種 Monoid。根據 Semigroup 的法則把兩個字串相接在一起的二元運算是 ++,而字串的單位元素,那個讓任何字串加上它,都還是本身的字串,就是…空字串 ""

  1. 字串的單位元素: ""
-- Haskell 語法
"foo" ++ "" == "" ++ "foo" == "foo"

串列

串列也是一種 Monoid。把兩個字串相接在一起的二元運算是 ++。單位元素是空陣列[]

-- Haskell 語法
[1, 2, 3] ++ [] == [] ++ [1, 2, 3] == [1, 2, 3]

整數加法是一種 Monoid 嗎?

是的。整數加法的單位元素是 Sum 0

整數乘法是 Monoid 嗎?

是的。整數乘法的單位元素是 Product 1

整數除法是 Monoid 嗎?

喔嗯。不好笑。

試試看

-- Haskell 語法
import Data.Monoid -- Data.Monoid 裡也有 Sum 及 Product

-- 在變數或值的後面加上 :: 來手動指定型別

mempty :: Sum Integer -- => Sum {getSum = 0}

mempty :: Product Integer -- => Product {getProduct = 1}

mempty :: [a] -- => []

mempty :: String -- => ""

為什麼要費神做這件事?

用來合併兩個值成為一個同型別值的二元函式,在所有的程式碼裡都非常常見。

就算在使用迴圈的指令式思考中,還是常常需要一個用來把結果放在一起的累加器。而我們選擇用什麼當累加器,其實就預告了這個迴圈最後一步是什麼。例如若選擇 0 當累加器,那麼這個迴圈,或迭代,或遞迴的最後一步,非常可能是個加法。而若是個空串列,那麼就會是把東西放到串列裡(appendpush 之類的)。

當意識到合併這個行為也可以抽象這件事之後,我們可以寫出這樣的程式碼:

-- 只在型別宣告標記這是個整數乘法,進行合併
foldr mappend mempty ([1, 3, 5] :: [Product Int]) -- => Product {getProduct = 15}

-- 型別為整數加法,進行合併
foldr mappend mempty ([1, 3, 5] :: [Sum Int]) -- => Sum {getSum = 9}

-- 字串的合併
foldr mappend mempty (["Hello", "World", "Haskell", "Rocks"]) -- => "HelloWorldHaskellRocks"

而我們之後會看到其它的 typeclass 裡,也會使用到類似的,合併兩個同型別的東西成為一個的概念。

Monoid 這個字是什麼意思?

這個字看起來不太像一般的英文。有可能是從數學上的廣群 groupoid 這個字變換過來的。不過在中文翻譯裡,有人叫它四分之三群 (比半群再多符合一條),亞群,或么半群

[to be continue]


上一篇
mostly:functional 第二十一章:Monoid 的法則
下一篇
mostly:functional 第二十三章:Monoid 的 Monoid
系列文
mostly:functional 從零開始的異世界程式觀 --- 函數式程式設計的試煉35

尚未有邦友留言

立即登入留言