所以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
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
你可能也想看
Google News 追蹤
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
2.0 上古漢語的特殊結構 2.3 之乎者也 —  也 (矣﹑焉) 2.3.1 也 一﹕初探之四 現在讓我們從函數引申出來的函子/論元觀點來解析上述「也」字的用法。用初級計算機科學編程的語言來說,函子就是一個具有函數功能的物件 (object),方便我們使用﹔它的功能就是讓我們可以召喚
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
Thumbnail
1.0 從函數到函算語法 1.4 函算語法 1.4.1 語法範疇理論導論 1.4.2 函算語法與函數概念 三 弗雷格從語言結構的觀點出發,提出了函數可以被視為一個不完整的表式。如果我們將一個函數拆解為一個由一個函子及其 (一個或多個) 論元所組成的表式,那麼該函子便是一個有待滿足的
主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
就是指變數可以被訪問和使用的範圍,來說一下var、let和const的作用域差異。 var :function example() { console.log(x); // 輸出: undefined 因為變量提升造成的 var x = 5; } 函數作用域或全域作用域 可以重複宣告
Thumbnail
這章節的目的是介紹 Kotlin 語言中函數的基本用法和概念,包括函數的聲明、使用、參數和返回值等。通過學習這章節,讀者可以熟練掌握如何在 Kotlin 中定義和使用函數,來解決各種問題。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 數學中函數概念的重要性難以盡書,亦很難想像沒有函數概念的數學可以走多遠。誇張一點,我們可以說很大部份的數學都是按函數概念操作的。但少有人留意到,在某個意義上,函數可說是數學語言的一個語構處理。 漢語「函數」一詞乃
Thumbnail
本章節旨在介紹 C# 中函數的基本結構,包括訪問修飾符、返回類型、方法名稱、參數列表和方法體。同時,也介紹了函數的各種呼叫方式、參數傳遞方式和返回值類型。讀者可以通過本章節,深入理解 C# 中函數的使用和應用。
※ 函式基礎介紹: ※ JavaScript 特殊的函式特性: 函式可以當成值來傳遞 (可以放進變數或放進物件) 函式可以當成函式的參數 callback - 在特定事件中觸發函式 (非同步特性) ※ 函式的基本寫法: ※ 調用 (invoke) 函式: "調用" 意指呼叫或執行
Thumbnail
隨著理財資訊的普及,越來越多台灣人不再將資產侷限於台股,而是將視野拓展到國際市場。特別是美國市場,其豐富的理財選擇,讓不少人開始思考將資金配置於海外市場的可能性。 然而,要參與美國市場並不只是盲目跟隨標的這麼簡單,而是需要策略和方式,尤其對新手而言,除了選股以外還會遇到語言、開戶流程、Ap
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
2.0 上古漢語的特殊結構 2.3 之乎者也 —  也 (矣﹑焉) 2.3.1 也 一﹕初探之四 現在讓我們從函數引申出來的函子/論元觀點來解析上述「也」字的用法。用初級計算機科學編程的語言來說,函子就是一個具有函數功能的物件 (object),方便我們使用﹔它的功能就是讓我們可以召喚
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
Thumbnail
1.0 從函數到函算語法 1.4 函算語法 1.4.1 語法範疇理論導論 1.4.2 函算語法與函數概念 三 弗雷格從語言結構的觀點出發,提出了函數可以被視為一個不完整的表式。如果我們將一個函數拆解為一個由一個函子及其 (一個或多個) 論元所組成的表式,那麼該函子便是一個有待滿足的
主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
就是指變數可以被訪問和使用的範圍,來說一下var、let和const的作用域差異。 var :function example() { console.log(x); // 輸出: undefined 因為變量提升造成的 var x = 5; } 函數作用域或全域作用域 可以重複宣告
Thumbnail
這章節的目的是介紹 Kotlin 語言中函數的基本用法和概念,包括函數的聲明、使用、參數和返回值等。通過學習這章節,讀者可以熟練掌握如何在 Kotlin 中定義和使用函數,來解決各種問題。
Thumbnail
本章節旨在介紹TypeScript中的函數,包括其基本結構、如何呼叫函數、函數的參數以及函數的返回值等相關概念。通過本章節,讀者可以學習到如何在TypeScript中使用不同的方式來定義函數,如函數聲明、函數表達式、箭頭函數和匿名函數等。
Thumbnail
1.0 從函數到函算語法 1.2 函數概念小史 1.2.1 中譯的來源 數學中函數概念的重要性難以盡書,亦很難想像沒有函數概念的數學可以走多遠。誇張一點,我們可以說很大部份的數學都是按函數概念操作的。但少有人留意到,在某個意義上,函數可說是數學語言的一個語構處理。 漢語「函數」一詞乃
Thumbnail
本章節旨在介紹 C# 中函數的基本結構,包括訪問修飾符、返回類型、方法名稱、參數列表和方法體。同時,也介紹了函數的各種呼叫方式、參數傳遞方式和返回值類型。讀者可以通過本章節,深入理解 C# 中函數的使用和應用。
※ 函式基礎介紹: ※ JavaScript 特殊的函式特性: 函式可以當成值來傳遞 (可以放進變數或放進物件) 函式可以當成函式的參數 callback - 在特定事件中觸發函式 (非同步特性) ※ 函式的基本寫法: ※ 調用 (invoke) 函式: "調用" 意指呼叫或執行