Haskell Tutorial(25)可被映射盒中物的 Functor << 前情
在〈Haskell Tutorial(25)可被映射盒中物的 Functor〉中,你應該知道一個 Functor 的行為,是能在 fmap 被指定對應的函式之後,從 f a 到 f b ,在還沒有談到 Functor 前,其實已經看過類似的行為,也就是 List 與 map ,Functor 不過就是進一步將這個行為規範出來並命名為 fmap 而已,因此,重點在於瞭解 fmap …
從 map 開始
List 的 map 是什麼樣的行為?它的型態是 (a -> b) -> [a] -> [b] ,這你很熟悉了,從另一個角度來看呢?如果 f 是個 a -> b 的函式,map f 呢?Haskell 中函式可以部份套用,因此 map f 會傳回一個函式,型態是 [a] -> [b] ,例如:

因此,從這個角度來看,你可以定義 add2 = map (+2) ,這樣就可以得到將 [Integer] 對應 [Integer] 的函式,也就是你就可以使用 add2 [1, 2, 3] 得到 [2, 3, 4] ,你也可以定義 lengthes = map length ,得到將 [[a]] 對應至 [Int] 的函式,這樣你就可以使用 lengthes ["Justin", "Monica", "Irene"] 得到 [6, 6, 5] 。
那麼,如果是 map id 呢?id 是個恒等函式(Identity function),型態是 a -> a ,它做的事只是將給定的引數做為傳回值,不做多餘的事,因此,map id 的結果就是得到一個 [b] -> [b] 的函式,例如:

這個例子在告訴你,map 不會做多餘的事,如果你要的對應是將原 List 中的元素逐個對應至新的 List,map 也是如實完成,本來就該如何,你不會希望 map 在對應的過程中,隱藏了某些你指定的函式外的多餘行為。
另一個你希望 map 要遵守的事是:

也就是說,如果你分開對 List 做數個 map 與對應函式的指定,結果應該與這些函式的函式合成相同,這樣我們才能視可讀性等情況採取想要的方式,而結果是相同的。
檢視 fmap 的實作
透過檢視熟悉的 List 與 map ,我們可以瞭解到 Functor 的 fmap 實際上是什麼,也可以瞭解它應當遵守哪些條例,fmap 的型態是 (a -> b) -> f a -> f b ,它接受一個函式與一個 f a 型態的值,然後傳回 f b 的值,從另一個角度來看呢?如果 f 是個 a -> b 的函式呢?
Haskell 中函式可以部份套用,因此 fmap f 會傳回一個函式,型態是 f a -> f b ,也就是 fmap 其實能將 a -> b 的函式提昇(Lift)為一個 f a -> f b 的函式,在上頭,map 也是,其實它是能將一個 a -> b 的函式提昇為一個 [a] -> [b] 的函式。
因此,你可以定義一個 add2 = fmap (+2) ,型態是 (Functor f, Num b) => f b -> f b ,這樣你就可以使用這個函式,將 Just 3 對應至 Just 5 ,將 [1, 2, 3] 對應至 [3, 4, 5] …

也就是說,fmap 在指定一個函式後傳回的函式,可以將一個 Functor 實例對應至另一個 Functor 實例,兩個 Functor 實例的型態相同,但內含值不同。
實際上,在〈Haskell Tutorial(25)可被映射盒中物的 Functor〉的範例中,findZipCode 、findCity 的函式型態早就說明了這點,它們分別是 Maybe String -> Maybe Int 與 Maybe String -> Maybe String ,實際上這兩個函式也可以寫成 Point free 風格:
findZipCode :: Maybe String -> Maybe Int
findZipCode = fmap zipCode
findCity :: Maybe String -> Maybe String
findCity = fmap city
我們需要的就只是 Maybe String -> Maybe Int 與 Maybe String -> Maybe String 的函式,而這可以透過 fmap zipCode 與 fmap city 來得到。
既然如此,fmap 該做的事,就只是按照我們指定的函式做對應,不應該做多餘的事,因此,對 fmap 指定 id ,傳回的函式在進行 Functor 的對應,其結果應該與對 Functor 執行 id 相同。例如:

進一步地,fmap 指定的函式,若為數個函式合成,那對 Functor 的執行結果,應與數個函式分別進行 fmap 相同。例如:

Functor 定律
最後兩個例子與陳述點出了 Functor 在實作 fmap 時應該遵合的定律,這在 Data.Functor 的文件中也有定義:
fmap id == id
fmap (f . g) == fmap f . fmap g
以上兩條分別對應的陳述就是:
fmap 該做的事,就只是按照我們指定的函式做對應,不應該做多餘的事,因此,對 fmap 指定 id ,傳回的函式在進行 Functor 的對應,其結果應該與對 Functor 執行 id 相同。
fmap 指定的函式,若為數個函式合成,那對 Functor 的執行結果,應與數個函式分別進行 fmap 相同。
說穿了沒什麼,其實就是要遵指定的守函式既有之行為,fmap 就是提供 Functor 與 Functor 間的對應,不當有額外的行為,就像物件導向中,子類別實作父類別或介面的方法時,應該遵守方法訂下的既有行為,不應該有額外的行為,像是如果父類別或介面中規範方法時,沒有副作用,子類別實作時就不應該產生副作用這類的規範。
後續 >> Haskell Tutorial(27)可直接函式套用的 Applicative
|