所以Monad 到底是什麼 — Covariance

閱讀時間約 10 分鐘

前一篇文章討論了 Functor 作為容器的意義,它提供了操作內容物的方法,並保持容器的結構不變。然而並不是只有容器才能是 Functor,例如(純)函式本身也是一種 Functor。你可以把函式看作是「帶有洞的值」,例如 (\a -> 1 + a * 2) 是一個接受整數並回傳整數的函式,你可以把他寫成帶有佔位符的表達式 (1 + _ * 2),而對它做 map 就只是把這個帶有佔位符的表達式帶入應用的函式,這會形成新的帶有佔位符的表達式:

instance functorFn :: Function a where
map func expr_with_hole = (func $ expr_with_hole _)

他並不是容器內實際的值,而是「帶有洞」的值,而 map 就是對這個抽象的值做操作。類似的,Promise 是代表「未來」的值,對他做 map 時會等到正確時機才應用函式;Lazy 代表直到需要時才會建構出來的值,對他做 map 其實就是增加一個建構步驟;IO 代表與外部世界互動後得到的值,對它做 map 就只是對結果做後處理。他們同時也是 Monad,這部分留到以後再說。

這些例子顯示「容器」並不是對 Functor 合適的描述,Functor 裡的「值」不一定是實際存在的值,而可以是只在「某種情況下」存在的值,我們把它稱為情境(context)。例如 Function a 是「依賴於 a」的情境;Promise 是「未來」的情境;IO 是「與外界互動」的情境。容器也是一種情境,例如 Maybe 是「有值或無值」兩種狀態的情境;Pair 是「第一個和第二個」的情境;List 是「N 個中第 i 個」的情境;Tree 是「這棵樹的某個節點」的情境。我們可以應用任何(純)函式做 map,甚至把外面的值帶入情境,這代表可以對這個情境下的值做「任何」操作,因此它可以看成是實際的值。它不只能做「任何」操作,有些情境還可以賦予其他能力,例如 IO 允許我們在這個情境下與外部世界互動。你可以想像它在內部自成一個程式的世界,這個世界至少擁有所有純函式的能力。

雖然能夠任意處理情境下的值,但是不能從情境中把值取出來,或者說沒有統一的方法。例如 List 需要透過 index 取值;Maybe 不一定有值,因此不一定取得出來;IO 雖然有值但無法取出,如果可以直接取出,我們就能夠複製 IO 本身(也就是外部世界),並對它做不同的 IO 操作(產生平行宇宙),然後取得各個操作後的結果。反過來說,即使現在也能夠複製 IO 本身,但是如果取不出來也沒辦法用。這跟線性特性有關,未來有機會再說。Functor 本來描述的就不是關於取出這件事,如果你真的想要這樣的能力,Comonad 有類似的關聯函式。除了不能從情境中取值,我們也不能根據情境決定如何處理值,例如對 List 做 map 就必須一視同仁地對所有元素處理,若真要根據位置處理可以使用 mapWithIndex。因此使用 map 時必須專注於值而非情境,就像使用 multicursor 或是使用 macro 編輯程式一樣,你必須要從特定的狀況、上下文抽離出來(例如第幾行、在哪個變數底下、哪個括號內等),執行一些與上下文無關的操作,否則可能會把程式碼改壞。

總結目前對於 Functor 的描述:如果 f 是 Functor,f a 就是代表在具有類型 f 的「情境」下具有類型 a 的「值」,而情境與值之間沒有依賴關係。這裡的 a 可以是任意的類型,這代表情境與值之間本來就不可能有依賴關係。所以這代表所有 f :: Type -> Type 都是 Functor?舉一個反例:newtype Predicate a = Predicate (a -> Boolean) 並不是 Functor,我們不可能給定 func :: a -> b 構造出 Predicate b(也就是 b -> Boolean)。Predicate a 其實是 Function Boolean a 的相反(把箭頭反過來),只是這裡的 a 代表的並不是「值」而是「洞」,因此沒辦法對它應用函式。但如果給定 func :: b -> a 倒是能構造出 Predicate b,其實就是把這個函式接在前面而已。我們可以用類似的規則定義相應的 typeclass:

class Contravariant f where
cmap :: (b -> a) -> (f a -> f b)

其中 cmap 必須遵守規則:

cmap f >>> cmap g = cmap (g >>> f)

這個 typeclass 描述的是:如果 f 是 Contravariant,f a 就是代表在具有類型 f 的「情境」下具有類型 a 的「洞」,而情境與洞之間沒有依賴關係。「洞」代表的是我們必須提供數值給他,而不是它會給我們數值。對這個洞做 cmap 可以看作是對「洞」應用函式。舉幾個是 Contravariant 的例子:Predicate 描述「做判斷」情境下的洞,你需要提供一個值給他,他最後會告訴你是與否;newtype Comparison a = Comparison (a -> a -> Ordering) 描述「比較」情境下的洞,你需要提供兩個值給他(在左手邊和右手邊這兩個情境下),他最後會告訴你誰大誰小;Const Unit 理所當然也是 Contravariant,它什麼都沒做。Contravariant 基本上都會跟函式的參數有關,因此很難找到其他不同又實用的範例,但如果允許函式具有副作用,可以從其他語言找到一些例子:rust 的 drop 或是 c++ 的 delete 很像 Contravariant,它會吃掉數值本身;golang 的 send-only channel 也很像 Contravariant,他會傳送提供給他的數值到另一端的 channel。

Contravariant 被稱作逆變,它是相對於 Functor 的另一個名字 Covariant 協變的概念。這兩個概念和 Java 等物件導向語言裡類型約束的協變和逆變基本上是同樣的概念,只是這裡描述的不是子類關係。並不是所有的 f :: Type -> Type 都是 Functor 或是 Contravariant,例如 newtype Func a = Func (a -> a),我們沒辦法給定 func :: a -> b 或 func :: b -> a 就建構出 Func b。事實上如果不考慮類型代表的語意,只要知道實際結構,很容易就能看出它是 Functor 還是 Contravariant 或都不是。規則很簡單:協變為正逆變為負,兩者的組合則為符號相乘。例如 type Ex1 a = Tuple (Either a Int) String,因為 Tuple 和 Either 的兩個類型參數都是協變的,正正得正,因此類型參數 a 是協變的;type Ex2 a = Tuple a Int -> Int,箭頭左邊是逆變的,Tuple 的左邊是協變的,負正得負,因此 a 是逆變的;type Ex3 a = Tuple Int a -> a,這裡的 a 同時出現在協變和逆變的位置,因此它兩者都不是;這個例子很有趣 type Ex4 a = (a -> Int) -> Number,a 位於兩層函式中逆變的位置,負負得正,因此他是協變的。為了證明它的確是 Functor,讓我們試著實作:

instance functorEx4 :: Functor Ex4 where
map :: forall a b. (a -> b) -> (Ex4 a -> Ex4 b)
map func ex = \b_int -> ex (func >>> b_int)

給定 func :: a -> b 和 ex :: (a -> Int) -> Number,為了建構結果 (b -> Int) -> Number,必須把它拆開來看,先把結果寫成 \b_int -> _,我們要設法補上 _ :: Number,但這時我們已經知道了 b_int :: b -> Int,再配合前面的給定參數就能建構出結果。這裡的 a 看似是洞,但事實上要建構出這個結構,勢必要先算出數值才能丟給傳入的函式,例如

average :: List a -> Ex4 a
average list = \score ->
let
count = length list
scores = map score list
total_score = toNumber (sum scores)
in total_score / count

其中必須先有 list :: List a 才能使用 score :: a -> Int,因此它的確是在某種情境下的「值」。事實上只要知道實際結構,就能輕鬆地建構出 Functor/Contravariant 的實作,而且這個實作一定是唯一的,這代表 Functor/Contravariant 並不像一般的 typeclass/interface,它描述的不是某種能力,而是一種與語意無關的理論。這個理論告訴你何謂「值」與「洞」,和你可以怎麼「取代」作為值/洞的類型參數。

4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
不怎麼完美的12月,難忘的不愛的抹茶蛋糕 自由飛翔的藝術和愛情,飛過了布魯克林的大橋,最後,永遠剩下藝術與她相伴,永遠不變的永恆,從小到大其他人是過客如浮雲。 她與他在唱歌痛快斯吼著,舉起手旋轉,說要地老天荒,多少次還是剩下她一個人與心碎。 身邊的男伴如浮雲來去,沒習慣也早已習慣,直到有天和她
護理位在轉變的世代,而時代進步的路上,新舊觀念的衝突難免,但我們是否具備接受「改變」的勇氣?夾好乾淨的頭髮跟維持儀容就是展現專業形象,那帶著徹夜未眠的疲憊上班,想問疲勞駕駛跟給藥錯誤嚴重度是否相當? 當世代間的隔閡深化,那高聳金字塔腐壞跟停滯也是可預期的,實在難以壯大、難以改變,嘖嘖嘖。
Thumbnail
PM要我們分享一個自己去過最寒冷的國家或景點,到目前為止,我只出過兩次國,一次是去Okinawa,另一次是去大陸坐船遊長江三峽,但兩次出國都在夏天,一點也不冷,還熱得要命!所以我去過最寒冷的國家,竟然是——
Thumbnail
話說我在〈歲末年終感恩文:感謝一眾帥哥美女的相助與支持!〉裡,曾經深深深深懷疑薯友天老(一個人的天荒地老)有雙重人格,不然怎麼會平常都貼學術論文,可當起優質創作者評審時,又能以生花妙筆為一眾創作者寫出精采絕倫的專屬模板文?
Thumbnail
上一次在診間昏倒後,醫生懷疑可能和我的血糖低有關係(因為當時所有數據中只有這項有問題),所以安排我去內分泌科檢查。接著一連串的檢查...
所以當我們遇到業力現前,懺悔業障,甘心忍受,一心念佛──道晟法師
  所以說,為什麼要長壽呢?   我現在正努力地賺錢,為了可以安心退休。
Thumbnail
不過找遍整個網路,談到「槓桿ETF報酬期望值」的人,除了我們之外,就只有PTT DAZE。
Thumbnail
採訪高管,不免擔憂多於興奮,憂心問答制式,或是與部門實際工作樣貌有著巨大落差。「來吧請坐!今天我們要聊聊什麼?」正與同仁聊天的文鎧副總抬起頭,像是家中爸爸一般微笑招呼,國泰置地廣場 27 樓的會議室,一瞬間成了溫馨客廳。
Thumbnail
關於「自己」這回事,在某個年紀就拋掉那些青少年時讀的那些關於自己的勵志暢銷書後,就沒再積極看市面上任何關於「自己」的任何書籍,但那種四肢無法伸展,總在人群裡不知道手腳怎麼擺的慌亂感,仍然時不時地從心裡蜿蜒出藤蔓布滿全身,讓人無法喘口氣,彷彿無時無刻都在提醒著「你得面對自己」。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
不怎麼完美的12月,難忘的不愛的抹茶蛋糕 自由飛翔的藝術和愛情,飛過了布魯克林的大橋,最後,永遠剩下藝術與她相伴,永遠不變的永恆,從小到大其他人是過客如浮雲。 她與他在唱歌痛快斯吼著,舉起手旋轉,說要地老天荒,多少次還是剩下她一個人與心碎。 身邊的男伴如浮雲來去,沒習慣也早已習慣,直到有天和她
護理位在轉變的世代,而時代進步的路上,新舊觀念的衝突難免,但我們是否具備接受「改變」的勇氣?夾好乾淨的頭髮跟維持儀容就是展現專業形象,那帶著徹夜未眠的疲憊上班,想問疲勞駕駛跟給藥錯誤嚴重度是否相當? 當世代間的隔閡深化,那高聳金字塔腐壞跟停滯也是可預期的,實在難以壯大、難以改變,嘖嘖嘖。
Thumbnail
PM要我們分享一個自己去過最寒冷的國家或景點,到目前為止,我只出過兩次國,一次是去Okinawa,另一次是去大陸坐船遊長江三峽,但兩次出國都在夏天,一點也不冷,還熱得要命!所以我去過最寒冷的國家,竟然是——
Thumbnail
話說我在〈歲末年終感恩文:感謝一眾帥哥美女的相助與支持!〉裡,曾經深深深深懷疑薯友天老(一個人的天荒地老)有雙重人格,不然怎麼會平常都貼學術論文,可當起優質創作者評審時,又能以生花妙筆為一眾創作者寫出精采絕倫的專屬模板文?
Thumbnail
上一次在診間昏倒後,醫生懷疑可能和我的血糖低有關係(因為當時所有數據中只有這項有問題),所以安排我去內分泌科檢查。接著一連串的檢查...
所以當我們遇到業力現前,懺悔業障,甘心忍受,一心念佛──道晟法師
  所以說,為什麼要長壽呢?   我現在正努力地賺錢,為了可以安心退休。
Thumbnail
不過找遍整個網路,談到「槓桿ETF報酬期望值」的人,除了我們之外,就只有PTT DAZE。
Thumbnail
採訪高管,不免擔憂多於興奮,憂心問答制式,或是與部門實際工作樣貌有著巨大落差。「來吧請坐!今天我們要聊聊什麼?」正與同仁聊天的文鎧副總抬起頭,像是家中爸爸一般微笑招呼,國泰置地廣場 27 樓的會議室,一瞬間成了溫馨客廳。
Thumbnail
關於「自己」這回事,在某個年紀就拋掉那些青少年時讀的那些關於自己的勵志暢銷書後,就沒再積極看市面上任何關於「自己」的任何書籍,但那種四肢無法伸展,總在人群裡不知道手腳怎麼擺的慌亂感,仍然時不時地從心裡蜿蜒出藤蔓布滿全身,讓人無法喘口氣,彷彿無時無刻都在提醒著「你得面對自己」。