Haskell Tutorial(26)Functor 的 fmap 行為


在〈Haskell Tutorial(25)可被映射盒中物的 Functor〉中,你應該知道一個 Functor 的行為,是能在 fmap 被指定對應的函式之後,從 f af b,在還沒有談到 Functor 前,其實已經看過類似的行為,也就是 List 與 mapFunctor 不過就是進一步將這個行為規範出來並命名為 fmap 而已,因此,重點在於瞭解 fmap

從 map 開始

List 的 map 是什麼樣的行為?它的型態是 (a -> b) -> [a] -> [b],這你很熟悉了,從另一個角度來看呢?如果 f 是個 a -> b 的函式,map f 呢?Haskell 中函式可以部份套用,因此 map f 會傳回一個函式,型態是 [a] -> [b],例如:

List 的 map

因此,從這個角度來看,你可以定義 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] 的函式,例如:

List 的 map

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

另一個你希望 map 要遵守的事是:

List 的 map

也就是說,如果你分開對 List 做數個 map 與對應函式的指定,結果應該與這些函式的函式合成相同,這樣我們才能視可讀性等情況採取想要的方式,而結果是相同的。

檢視 fmap 的實作

透過檢視熟悉的 List 與 map,我們可以瞭解到 Functorfmap 實際上是什麼,也可以瞭解它應當遵守哪些條例,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]

Functor 的 fmap

也就是說,fmap 在指定一個函式後傳回的函式,可以將一個 Functor 實例對應至另一個 Functor 實例,兩個 Functor 實例的型態相同,但內含值不同。

實際上,在〈Haskell Tutorial(25)可被映射盒中物的 Functor〉的範例中,findZipCodefindCity 的函式型態早就說明了這點,它們分別是 Maybe String -> Maybe IntMaybe String -> Maybe String,實際上這兩個函式也可以寫成 Point free 風格:

findZipCode :: Maybe String -> Maybe Int
findZipCode = fmap zipCode

findCity :: Maybe String -> Maybe String
findCity = fmap city

我們需要的就只是 Maybe String -> Maybe IntMaybe String -> Maybe String 的函式,而這可以透過 fmap zipCodefmap city 來得到。

既然如此,fmap 該做的事,就只是按照我們指定的函式做對應,不應該做多餘的事,因此,對 fmap 指定 id,傳回的函式在進行 Functor 的對應,其結果應該與對 Functor 執行 id 相同。例如:

Functor 的 fmap

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

Functor 的 fmap

Functor 定律

最後兩個例子與陳述點出了 Functor 在實作 fmap 時應該遵合的定律,這在 Data.Functor 的文件中也有定義:

fmap id  ==  id
fmap (f . g)  ==  fmap f . fmap g

以上兩條分別對應的陳述就是:

  1. fmap 該做的事,就只是按照我們指定的函式做對應,不應該做多餘的事,因此,對 fmap 指定 id,傳回的函式在進行 Functor 的對應,其結果應該與對 Functor 執行 id 相同。
  2. fmap 指定的函式,若為數個函式合成,那對 Functor 的執行結果,應與數個函式分別進行 fmap 相同。

說穿了沒什麼,其實就是要遵指定的守函式既有之行為,fmap 就是提供 FunctorFunctor 間的對應,不當有額外的行為,就像物件導向中,子類別實作父類別或介面的方法時,應該遵守方法訂下的既有行為,不應該有額外的行為,像是如果父類別或介面中規範方法時,沒有副作用,子類別實作時就不應該產生副作用這類的規範。