iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 25
1
Software Development

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

mostly:functional 第二十四章:Functor 的法則

…人被視為一個歷程,一個成為 (becoming) 的歷程。該模式相信,每一個人都可能改變。即使外在的改變很有限,內在的改變卻是可能的。這個信念是普世皆然、毫無限制的,它結合了終生皆有可塑性的觀點。……成為是流動性的,隨時都在改變,從一連串的抉擇當中開展。我們總是在成為什麼的過程中……

-- 約翰·貝曼, 薩提爾成長模式的應用


-- 0902
真的忘記為了解開那個題目,來回這裡多少次了。

不過解開的那一瞬間真是愉快啊。我還記得在整棟樓房亮起的那個時候,外面似乎有個低鳴的聲音。擔心又來一次城市崩毀,我衝出去探探究竟。

卻是城牆前有一整區的建築變得鮮明起來。其中最前方那一整面,更是遮蔽了原先可以一眼望過去的視野,城裡再也不似之前空若無物的樣子了。

一如以往,那塊泛紫金屬板又停在那邊了。我摸了一下,想了想,還是問了:「你有看過一隻棕紅色的小動物嗎?」

板子的表面上浮現了這個字樣:

Nothing <> Just "a book"

向上看,這一棟建築上的符號是:<$>。我記得的,這棟跟之前若有似無的樣子真是天壤之別。

Functor 的實作

class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
{-# MINIMAL fmap #-}

Functor 的法則:

  1. 封閉律
  2. 單位元素 (Identity)
  3. 分配律



Functor,跟它的中文翻譯:函子,雖然看起來一付很深奧的樣子,但其實概念上非常簡單。可以說寫程式一陣子的人裡,應該很難找到沒用過的。

每當你使用了串列/陣列的 map,你就是在使用它的 Functor 特質所帶來的好處,而我們在之前已經看過好幾次了。

fmap:map for Functor

若是以串列/陣列當做容器,那麼 fmap 跟大家習慣的 map 是一模一樣的。另外 fmap 還有一個中綴版的符號:<$>

-- Haskell 語法

map  (+1) [1, 2, 3] -- => [2, 3, 4] -- 串列就是串列
fmap (+1) [1, 2, 3] -- => [2, 3, 4] -- 把串列當 functor

-- 中綴版
(+1) <$> [1, 2, 3] -- => [2, 3, 4]

然而 functor 的優秀之處,並不僅限於串列。或者應該這麼說,當我們可以將 map 的概念,用於其它容器上時,談 Functor 才比較有意義。

若能把這個「改變容器內容,而維持外殼不變」的特質也抽象出來,那麼就會得出一個堪稱作弊模式的觀點:升格 (Lifting)。函子最有威力的視角,不在於改變容器的內容,而是把函式升格這件事,以及取得這個抽象後的運用手法。

看起來沒什麼用的東西:$

為了要解釋什麼是升格,我們再來說明一個看起來沒什麼功能的函式:

($) :: (a -> b) -> a -> b
infixr 0 $

(+ 2) $ 10 -- -> 12

嗯… 這操作起來跟單純的函式呼叫一模一樣,那明明就用空白就可以啦?不過當我們試試看下面的寫法時,就會出現問題。

(+1) . (+2) 10

-- 出錯了!
-- => <interactive>:90:1: error:
--    • Non type-variable argument in the constraint: Num (a -> c)
--      (Use FlexibleContexts to permit this)
--    • When checking the inferred type
--        it :: forall c a. (Num c, Num (a -> c)) => a -> c

要問為什麼會這樣,原因是用空白來調用函式的優先順序極高,所以在上述的程式碼中,會先用 10 當引數來調用 (+ 2) 函式得到 12。接著拿著這個數字試著與 (+1). 進行函式組合時就會出錯了。這個時候,我們可以選擇用括號把前面括起來:

-- Haskell 語法
((+1) . (+2)) 10 -- => 13

再看一下剛剛 $ 的定義,仔細看的話,會發現它的優先度是…0,最低優先。所以我們可以使用 $ 來讓函式應用的順序變成最後才應用,來減少括號的干擾:

-- Haskell 語法
(+1) . (+2) $ 10 -- => 13

升格 (Lifting)

讓我們來比較一下 $,也就是一般的函式應用 (apply) 與 fmap,或說 <$> 的型別定義:

-- Haskell 語法

$   ::  (a -> b) ->   a ->   b
<$> ::  (a -> b) -> f a -> f b

仔細看一下,就會發現差別在當我們把 (a -> b) 的函式傳給 $ 後,我們拿到的就是一個原封不動的 a -> b 函式。相對於此 ,<$> 的第二個參數跟回傳值,都是包在 f 裡面的。這個 f 代表的是具有 Functor 特質的容器,你可以暫時把這個 f 想像成串列的外殼。

而把 Haskell 惰性求值一起放進來考慮時,我們可以這樣說:如果我們把一個 (a -> b) 的函式當做參數,傳給 fmap 後,我們得到了一個升級版的函式:f a -> f b。這個升級版的函式不是直接處理 a 型別,回傳 b 型別。而是能夠穿透容器的外殼,處理裝在 f 容器裡的 a 型別,並回傳裝在 f 容器裡的 b 型別。維持其外殼 f 不變。

所以 Haskell 裡是這麼說的:fmap 可以讓一個函式升格成變動容器內容的函式。

單位元素: id

而函式的單位元素,就是我們之前提過的恆等函式:id。而用法則是這樣的:

-- Haskell 語法
fmap id = id

-- 試試看。對某個 functor 用 id 去 fmap,等同於把該 functor 直接傳入 id 函式。
fmap id "Hi functor" -- => "Hi functor"
id "Hi functor" -- => "Hi functor"

分配律:升格與組合

再來看一下數學,例如下面這個式子,我們會說乘法對加法符合分配律
https://chart.googleapis.com/chart?cht=tx&chl=3%20%5Ctimes%20(1%20%2B%202)%20%3D%203%20%5Ctimes%201%20%2B%203%20%5Ctimes%202

那麼對於 Functor 來說,fmap函式組合也符合分配律:
fmap (f . g) 等同於 fmap f . fmap g

當你用 fmap 來升格 f . g 這個函式組合,會等於於將 fg 分別升格之後,再進行函式組合。

-- Haskell 語法
f = (+1)
g = (*3)

fmap (f . g) $ [1, 2, 3] -- => [4,7,10]

fmap f . fmap g $ [1, 2, 3] -- => [4,7,10]



我本來想隨便丟個串列進去門上那個孔的,但是當我手上拿著空的串列靠進門的時候,伴隨著門上的光線很不明顯的變暗了一下,我似乎聽到了一聲很輕的嘆息。我想,可能它吃這個吃得很膩了吧。我走遠了一點四處翻找,終於讓我找到一個 Nothing

[to be continue]


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

尚未有邦友留言

立即登入留言