Haskell Tutorial(25)可被映射盒中物的 Functor


之前提過很多次 Monad 了,接下來,我們要逐步認識 Monad,不過,不是直接認識它,而是先來認識 FunctorApplicative,它們跟 Monad 的精神類似,都是有關於值、情境(Context)、動作的指定,照例地,我們一步一步來,這一篇要先從認識 Functor 開始。

Funtor Typeclass

在 Haskell 中,Functor 是個 Typeclass,必須得實作的行為是:

class Functor f where 
    fmap :: (a -> b) -> f a -> f b

fmap 定義來看,它接受一個函式與一個 f a 型態的值,然後傳回 f b 的值,這是什麼?f a 型態?f b 型態?如果你看到這定義時,意識已經開始神遊,沒關係,我一開始也這樣 … XD

我還是認真一點好了,回想一下,在什麼時候,你會用兩個值來決定一個型態呢?Tuple?是的!這是個例子,不過它決定了一個沒有名稱的型態,還有嗎?在表示可能有也可能沒有結果時,我們會使用 Maybe 對吧!如果函式會傳回一個 Maybe,你可能得為它決定一個具體型態,像是 Maybe IntMaybe String 之類的,還有嗎?List!如果函式會傳回 List,你可能也得為它決定一個具體型態,像是 [Char][Int] 之類的。

所以,具有行為 Functor 的型態,其具有的行為是可以指定 a -> b 的函式,然後用這個函式幫你從 f a 對應到 f b,例如,你可以指定一個 String -> Int 的函式,用 fmap 與這個函式將一個 Maybe String,對應為 Maybe Int,或者,可以指定 Char -> Int,用 fmap 與這個函式將 [Char] 映射為 [Int]

Maybe Functor

程式中在處理有或沒有值的問題時,經常會從某個函式取得 Maybe,判斷是 Just somethingNothing 後採取進一步動作,例如:

findZipCode :: Maybe String -> Maybe Int
findZipCode (Just orderNum)  = Just (zipCode orderNum)
findZipCode Nothing          = Nothing

上面的 findZipCode 函式可以給個 Maybe String,其中 String 是訂單號碼,假設訂單上的位址中一定有郵遞區號資訊,那麼 zipCode 會取出訂單上郵遞區號,或許你也會想要取得城市資訊:

findCity :: Maybe String -> Maybe String
findCity (Just orderNum)  = Just (city orderNum)
findCity Nothing          = Nothing

顯然地,這具有類似的流程,只是當中使用了 zipCode 或是 city,既然如此,不如定義一個通用的 fmap 函式,讓使用者可以指定傳入 zipCode 或是 city 這類函式:

fmap :: (a -> b) -> f a -> f b
fmap f (Just x) = Just (f x) 
fmap f Nothing  = Nothing

那麼,findZipCodefindCity 就可以分別改寫如下:

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

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

實際上,Maybe 本身就具有 Functor 的行為,因此,你可以直接使用 fmap

Maybe Functor

對於 Maybe Functorfmap 在指定對應的函式之後,就可以將 Maybe A 對應至 Maybe B,它隱藏了有無值的判斷、Just 中值的取出、將 A 對應至 B 後包裝為 Just B 的行為,讓使用者只需指定從 A 對應至 B 的函式,以突顯程式意圖。

List Functor

只需指定 A 對應至 B 的函式,以突顯程式意圖,這句話聽起來很熟悉?List 的 map 不就是這個目的?像是 map length ["Justin", "Monica", "Irene"],可以將 [String] 對應至 [6, 6, 5]map (*3) [1, 2, 3] 可以將 [1, 2, 3] 對應至 [3, 6, 9],那麼 List 不就具有 Functor 的行為嗎?對的!你也可以使用 fmap 來做這些事:

List Functor

因為已經有了 map 函式,因此要 List 實現 Functor 的行為,只要令 fmapmap 就可以了:

instance Functor [] where 
    fmap = map

雖然你應該很熟悉 List 的 map 在做些什麼,不過還是來照樣造句一下,這有助於瞭解 Functor 定義了什麼行為,對於 List Functorfmap 可以在指定對應的函式之後,將 [A] 對應至 [B],它隱藏了走訪 List 的行為,將 A 對應至 B 後建立 [B] 的行為,讓使用者只需指定從 A 對應至 B 的函式,以突顯程式意圖。

因此,具有 Functor 行為的型態,可以將它想像成一個盒子,就像 Maybe 或 List 這類,它們在實現 Functor 的行為時(也就是 fmap),隱藏了某些運算,你可以指定函式告知 fmap,這盒子中的值如何對應(映射),這樣你就可以從這個盒子到那個盒子,就像從 Maybe String 對應為 Maybe Int,或是 [String] 對應至 [Int]

IO Functor

在 Haskell 中,你一定寫過以下類似的程式:

main = do
    input <- getLine
    let result = (show . (*2) . read) input
    putStrLn result

getLine 的傳回型態是 IO String,因此你要將結果綁定到一個名稱,以便傳給其他函式,在這邊其他函式是指 read(*2)show 這些函式,這些函式最後傳回了一個 Int

假設,最後傳回的型態可以是 IO Int 呢?也就是從 getLineresult,型態是 IO StringIO Int 呢?這聽起來像是 fmap 指定對應函式之後在做的事,而 show . (*2) . read 是個從 String 對應至 Int 的函式,這不就是可以指定給 fmap 的函式嗎?沒錯,你是可以這麼做的:

main = do
    result <- fmap (show . (*2) . read) getLine
    putStrLn result

也就是說,IO 也是個 Functor,在實作行為時的定義是這樣的:

instance Functor IO where 
    fmap f action = do 
        result <- action 
        return (f result) 

因此,如果你想要將 IO something 的結果綁定到一個名稱,以便傳給其他函式,可以考慮使用 fmap,讓程式看來簡潔一些。

最後要來個題目,按照 Functor 的定義,對應必須是 f af b,那 Either 可以是個 Functor 嗎?一個 Either a b 對應至 Either c d,顯然不符合 Functor 的定義,不過實際上,一個 Right 40 的型態是 Either a Integer,有沒有需求可以是 Right 40 對應至 Right "Justin" 呢?你可以試著讓 Either a 實現 Functor 的行為!