所以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。

avatar-img
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
have bear的沙龍 的其他內容
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
如果你曾經試圖學習函數式編程,並嘗試理解Monad,但看到文件上的定義卻一個字都看不懂,使用的術語、概念和一般常見的語言又很不一樣。網路上的教程往往都是以最簡單的範例試圖解釋Monad,但看到實際案例後又發現你完全不懂。事實上大部分教程的描述並不適用於「所有」的Monad,甚至在某方面來說是錯的,就
你可能也想看
Google News 追蹤
Thumbnail
大家好,我是woody,是一名料理創作者,非常努力地在嘗試將複雜的料理簡單化,讓大家也可以體驗到料理的樂趣而我也非常享受料理的過程,今天想跟大家聊聊,除了料理本身,料理創作背後的成本。
Thumbnail
哈囉~很久沒跟各位自我介紹一下了~ 大家好~我是爺恩 我是一名圖文插畫家,有追蹤我一段時間的應該有發現爺恩這個品牌經營了好像.....快五年了(汗)時間過得真快!隨著時間過去,創作這件事好像變得更忙碌了,也很開心跟很多厲害的創作者以及廠商互相合作幫忙,還有最重要的是大家的支持與陪伴🥹。  
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
這篇內容,將會講解什麼是表達式(Expression),什麼是陳述式(Statement)。有了這些概念,各位會更容易理解,要如何設計程式碼。
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
就是指變數可以被訪問和使用的範圍,來說一下var、let和const的作用域差異。 var :function example() { console.log(x); // 輸出: undefined 因為變量提升造成的 var x = 5; } 函數作用域或全域作用域 可以重複宣告
※ 函式基礎介紹: ※ JavaScript 特殊的函式特性: 函式可以當成值來傳遞 (可以放進變數或放進物件) 函式可以當成函式的參數 callback - 在特定事件中觸發函式 (非同步特性) ※ 函式的基本寫法: ※ 調用 (invoke) 函式: "調用" 意指呼叫或執行
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
前幾篇討論到各種裝飾器的用法,本文將介紹另外一種裝飾器,可以將方法轉換成屬性來使用。 property也可以動態的取出物件的值,隨著時間或其他運算改變所產生的值,讓我們繼續往下看更多介紹吧。
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。
Thumbnail
大家好,我是woody,是一名料理創作者,非常努力地在嘗試將複雜的料理簡單化,讓大家也可以體驗到料理的樂趣而我也非常享受料理的過程,今天想跟大家聊聊,除了料理本身,料理創作背後的成本。
Thumbnail
哈囉~很久沒跟各位自我介紹一下了~ 大家好~我是爺恩 我是一名圖文插畫家,有追蹤我一段時間的應該有發現爺恩這個品牌經營了好像.....快五年了(汗)時間過得真快!隨著時間過去,創作這件事好像變得更忙碌了,也很開心跟很多厲害的創作者以及廠商互相合作幫忙,還有最重要的是大家的支持與陪伴🥹。  
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
這篇內容,將會講解什麼是表達式(Expression),什麼是陳述式(Statement)。有了這些概念,各位會更容易理解,要如何設計程式碼。
Thumbnail
這篇內容,將會講解什麼是函式,以及與函式相關的知識。包括函式的簡介、Runtime Function、自訂函式、Script Function 腳本函式、Method 方法。
主要來講宣告函式跟箭頭函式 : 宣告函式(Function Declaration) 語法: function functionName(parameters) { return result; } 特點: 使用 function 關鍵字 函式名稱是必需的 存在函式
就是指變數可以被訪問和使用的範圍,來說一下var、let和const的作用域差異。 var :function example() { console.log(x); // 輸出: undefined 因為變量提升造成的 var x = 5; } 函數作用域或全域作用域 可以重複宣告
※ 函式基礎介紹: ※ JavaScript 特殊的函式特性: 函式可以當成值來傳遞 (可以放進變數或放進物件) 函式可以當成函式的參數 callback - 在特定事件中觸發函式 (非同步特性) ※ 函式的基本寫法: ※ 調用 (invoke) 函式: "調用" 意指呼叫或執行
Thumbnail
代理模式通過封裝原始對象來實現對該對象的控制和管理,同時不改變原始對象的行為或客戶端與該對象互動的方式,以此介入或增強對該對象的訪問和操作。
Thumbnail
前幾篇討論到各種裝飾器的用法,本文將介紹另外一種裝飾器,可以將方法轉換成屬性來使用。 property也可以動態的取出物件的值,隨著時間或其他運算改變所產生的值,讓我們繼續往下看更多介紹吧。
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。