所以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,它描述的不是某種能力,而是一種與語意無關的理論。這個理論告訴你何謂「值」與「洞」,和你可以怎麼「取代」作為值/洞的類型參數。

留言
avatar-img
留言分享你的想法!
avatar-img
have bear的沙龍
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
have bear的沙龍的其他內容
2024/07/18
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
2024/07/18
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
看更多
你可能也想看
Thumbnail
大家好,我是一名眼科醫師,也是一位孩子的媽 身為眼科醫師的我,我知道視力發展對孩子來說有多關鍵。 每到開學季時,診間便充斥著許多憂心忡忡的家屬。近年來看診中,兒童提早近視、眼睛疲勞的案例明顯增加,除了3C使用過度,最常被忽略的,就是照明品質。 然而作為一位媽媽,孩子能在安全、舒適的環境
Thumbnail
大家好,我是一名眼科醫師,也是一位孩子的媽 身為眼科醫師的我,我知道視力發展對孩子來說有多關鍵。 每到開學季時,診間便充斥著許多憂心忡忡的家屬。近年來看診中,兒童提早近視、眼睛疲勞的案例明顯增加,除了3C使用過度,最常被忽略的,就是照明品質。 然而作為一位媽媽,孩子能在安全、舒適的環境
Thumbnail
我的「媽」呀! 母親節即將到來,vocus 邀請你寫下屬於你的「媽」故事——不管是紀錄爆笑的日常,或是一直想對她表達的感謝,又或者,是你這輩子最想聽她說出的一句話。 也歡迎你曬出合照,分享照片背後的點點滴滴 ♥️ 透過創作,將這份情感表達出來吧!🥹
Thumbnail
我的「媽」呀! 母親節即將到來,vocus 邀請你寫下屬於你的「媽」故事——不管是紀錄爆笑的日常,或是一直想對她表達的感謝,又或者,是你這輩子最想聽她說出的一句話。 也歡迎你曬出合照,分享照片背後的點點滴滴 ♥️ 透過創作,將這份情感表達出來吧!🥹
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
Thumbnail
在這一章中,我們探討了 PHP 中的函數,包括函數的基本結構、不同的函數定義方式(如函數聲明、函數表達式、箭頭函數和匿名函數)以及如何呼叫函數。我們還討論了函數的參數處理方式,包括單個參數、多個參數、預設參數值和剩餘參數。此外,我們還介紹了函數的返回值,包括返回單個值、返回物件和返回函數的情況。
Thumbnail
在這一章中,我們探討了 PHP 中的函數,包括函數的基本結構、不同的函數定義方式(如函數聲明、函數表達式、箭頭函數和匿名函數)以及如何呼叫函數。我們還討論了函數的參數處理方式,包括單個參數、多個參數、預設參數值和剩餘參數。此外,我們還介紹了函數的返回值,包括返回單個值、返回物件和返回函數的情況。
Thumbnail
本章節主要介紹Java語言中的函數(也稱為方法)的使用,包括函數的基本結構、函數表達式(Lambda表達式)、箭頭函數、匿名函數的使用,以及如何呼叫函數、如何使用函數參數和函數的返回值等內容。通過學習本章節,讀者將能夠熟練掌握Java語言中的函數相關知識,並能夠在實際編程中靈活運用。
Thumbnail
本章節主要介紹Java語言中的函數(也稱為方法)的使用,包括函數的基本結構、函數表達式(Lambda表達式)、箭頭函數、匿名函數的使用,以及如何呼叫函數、如何使用函數參數和函數的返回值等內容。通過學習本章節,讀者將能夠熟練掌握Java語言中的函數相關知識,並能夠在實際編程中靈活運用。
Thumbnail
這章節的目的是介紹 Kotlin 語言中函數的基本用法和概念,包括函數的聲明、使用、參數和返回值等。通過學習這章節,讀者可以熟練掌握如何在 Kotlin 中定義和使用函數,來解決各種問題。
Thumbnail
這章節的目的是介紹 Kotlin 語言中函數的基本用法和概念,包括函數的聲明、使用、參數和返回值等。通過學習這章節,讀者可以熟練掌握如何在 Kotlin 中定義和使用函數,來解決各種問題。
Thumbnail
此章節旨在解釋Swift語言中函數的基本結構和操作方式,包括函數的聲明、呼叫、參數和返回值。閱讀這個章節可以幫助你理解並掌握如何在Swift編程中有效地使用和管理函數。
Thumbnail
此章節旨在解釋Swift語言中函數的基本結構和操作方式,包括函數的聲明、呼叫、參數和返回值。閱讀這個章節可以幫助你理解並掌握如何在Swift編程中有效地使用和管理函數。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
Function的使用方式
Thumbnail
Function的使用方式
Thumbnail
本章節旨在介紹 C# 中函數的基本結構,包括訪問修飾符、返回類型、方法名稱、參數列表和方法體。同時,也介紹了函數的各種呼叫方式、參數傳遞方式和返回值類型。讀者可以通過本章節,深入理解 C# 中函數的使用和應用。
Thumbnail
本章節旨在介紹 C# 中函數的基本結構,包括訪問修飾符、返回類型、方法名稱、參數列表和方法體。同時,也介紹了函數的各種呼叫方式、參數傳遞方式和返回值類型。讀者可以通過本章節,深入理解 C# 中函數的使用和應用。
Thumbnail
在Python中,我們可以用def關鍵字定義函數,並透過函數名稱呼叫它。函數參數可以是必填、關鍵字、默認或不定長度的類型。return語句負責結束函數並回傳值。全域變數可以在整個程序中使用,而區域變數只能在特定函數內使用。我們還可以在一個文件中定義函數,然後在另一個文件中呼叫它。
Thumbnail
在Python中,我們可以用def關鍵字定義函數,並透過函數名稱呼叫它。函數參數可以是必填、關鍵字、默認或不定長度的類型。return語句負責結束函數並回傳值。全域變數可以在整個程序中使用,而區域變數只能在特定函數內使用。我們還可以在一個文件中定義函數,然後在另一個文件中呼叫它。
Thumbnail
可選串聯(?.)運算符用於訪問 object 的屬性或調用函數。如果使用該運算符訪問的object 或調用的函式為 undefined 或 null,則表達式會回傳 undefined,而不是拋出錯誤。
Thumbnail
可選串聯(?.)運算符用於訪問 object 的屬性或調用函數。如果使用該運算符訪問的object 或調用的函式為 undefined 或 null,則表達式會回傳 undefined,而不是拋出錯誤。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News