在〈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
間的對應,不當有額外的行為,就像物件導向中,子類別實作父類別或介面的方法時,應該遵守方法訂下的既有行為,不應該有額外的行為,像是如果父類別或介面中規範方法時,沒有副作用,子類別實作時就不應該產生副作用這類的規範。