所以Monad到底是什麼 — Apply as Zippable

閱讀時間約 6 分鐘

Apply 是一種 Functor,它提供更多操作情境的能力:

class (Functor f) <= Apply f where
apply :: forall a b. f (a -> b) -> (f a -> f b)

這裡的 (Functor f) <= Apply f 代表如果 f 是 Apply,那麽它也是一種 Functor,一般來說我們必須先實作 Functor 才能實作 Apply。回顧一下 Functor,map 就像是把情境裡的值拿出來,做一些操作後再把結果放回情境裡,而 apply 就像是從情境中把函式和參數拿出來,呼叫後再把結果放回情境裡。從類型上來看,map func :: f a -> f b 是對一個情境下的值的操作,而 apply :: f (a -> b) -> f a -> f b 則是對兩個情境下的值的操作。對比直接對值做操作的函式 func :: a -> b 和 ($) :: (a -> b) -> a -> b,黏在參數和回傳類型前的 f 代表它只能在情境底下做操作,但仍然不能把值拿出來。如果是在同一個情境下(也就是 f (Tuple (a -> b) a))根本不需要 apply,只要 map 就能辦到。因此它的重點不在於應用函式,而是合併情境。map 能夠把一般的函式 func :: a -> b 提升成為情境下的操作 f a -> f b,而 apply 就像是能夠把情境底下的函式 boxed_func :: f (a -> b) 取出得到 func :: a -> b,再做提升 box_func :: f a -> f b。有了這個方法我們可以更進一步把有兩個參數的一般函式 func2 :: a -> b -> c 提升成 f a -> f b -> f c,這被定義為 lift2。更多參數的函式都能用類似的方式提升。注意,這裡的參數都是在各自的情境底下,因此它擁有合併情境的能力。使用這個方法可讓我們無視情境的隔閡操作裡面的值,情境就像包覆於表面的水,當兩者靠近時水膜就會融合成一體。

apply 的重點不是應用函式,而是合併情境,然而 Apply 的定義並不能很好地描述最重要的特性。如果要更清楚的描述合併情境的概念,可以定義 zip :: forall f a b. Apply f => f a -> f b -> f (Tuple a b),這個函式只把兩個情境下的值合併成 tuple,並不會做其他事。很容易可以證明 apply 和 zip 在 Functor 下是等價的。zip 可以是把具有相同意義的情境的元素對應起來的方法。例如,Pair 是 Apply,它的 zip 就是把第一個元素對應到第一個元素,第二個元素對應到第二個元素,因此 zip (Pair 1 2) (Pair 3 4) = Pair (Tuple 1 3) (Tuple 2 4)。這可以推廣到固定長度的陣列 Vec,其中 zip 就是把相同索引的元素靠在一起。不定長度的 List(嚴格來說是 ZipList)也是 Apply,其中 zip 同樣是根據索引將對應的元素靠在一起,如果某個陣列長度不夠就停止,例如 zip [1,2,3] [a,b] = [Tuple 1 a, Tuple 2 b]。Maybe,Map,Tree 等結構也可以使用類似方式定義 zip。

並不是只有一種方法能夠合併資料結構,例如可以把 Pair 的 zip 定義改成合併不同元素,因此 zip (Pair 1 2) (Pair 3 4) = Pair (Tuple 1 4) (Tuple 2 3)。然而這並不是合法的 Apply,因為它違反了結合律:

apply (apply (map compose f) g) h  =  apply f (apply g h)

這個規則寫起來很複雜,讓我們引入新的語法 applicative bang notation,可以當作是引入一個直接從 Functor 取值的函式 ! :: f a -> a,因此我們不再需要寫下 map 和 apply。規則變成:

(compose !f !g) !h  =  !f (!g !h)

從這個形式可以明白為什麼它叫做結合律。這裡的結合律指的是情境的結合律:合併情境的順序不應改變結果。回到 Pair 的例子,通過測試可以發現它並不符合結合律。同樣地,如果我們想用不同的順序對 Vec、List、Tree 等結構做 zip,也會違反結合律。簡單來說,只有對應相同情境的方式才有辦法合法地把兩個結構 zip 起來。

不只是容器類的 Functor 才能夠 zip,例如 Function a 也是 Apply,對兩個有洞的值做 zip 就是把傳入的值複製成兩個,並填進原本的兩個洞,這會形成新的有洞的值。簡單來說就是把兩個洞合併成一個,以此對齊兩者的情境。

zip a_with_hole b_with_hole =
case _ of value ->
Tuple (a_with_hole value) (b_with_hole value)

Function a 有沒有其它可能的 Apply 實作?很明顯並沒有,但如果給洞的類型加上限制,倒是有可能建構出其它的實作,如果參數可以分裂 split :: a -> Tuple a a,那麽就能用這個函式取代複製,得到新的實作:

zip a_with_hole b_with_hole =
case split _ of Tuple value1 value2 ->
Tuple (a_with_hole value1) (b_with_hole value2)

然而為了遵守結合律,split 需要符合一些特性,因此不是任意的分裂方法都可以。其中一個可能的實作是 data S = A | L | R | M,其中

split A = Tuple L R
split L = Tuple L M
split R = Tuple M R
split M = Tuple M M

事實上其他可能的實作都只是這個的變化型,因此實質上並沒有其他可能的結構。這代表 Function a 這類 Functor 基本上也只能擁有一種 zip 的實作,除非我們引入 linear type 的概念,但這又是另一段故事了。

然而並不是只有這種 zip 般的方法可以實現 Apply,下一篇文章將從其他角度實作 Apply。

    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😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
    Thumbnail
    這篇內容,將會講解什麼是表達式(Expression),什麼是陳述式(Statement)。有了這些概念,各位會更容易理解,要如何設計程式碼。
    Thumbnail
    這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
    主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
    如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
    就是指變數可以被訪問和使用的範圍,來說一下var、let和const的作用域差異。 var :function example() { console.log(x); // 輸出: undefined 因為變量提升造成的 var x = 5; } 函數作用域或全域作用域 可以重複宣告
    Thumbnail
    本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。
    Thumbnail
    這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
    Thumbnail
    11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
    Thumbnail
    Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
    Thumbnail
    這篇內容,將會講解什麼是表達式(Expression),什麼是陳述式(Statement)。有了這些概念,各位會更容易理解,要如何設計程式碼。
    Thumbnail
    這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
    主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
    如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
    就是指變數可以被訪問和使用的範圍,來說一下var、let和const的作用域差異。 var :function example() { console.log(x); // 輸出: undefined 因為變量提升造成的 var x = 5; } 函數作用域或全域作用域 可以重複宣告
    Thumbnail
    本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。