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。