Haskell Tutorial(24)拋出與自訂 Exception << 前情
之前提過很多次 Monad 了,接下來,我們要逐步認識 Monad,不過,不是直接認識它,而是先來認識 Functor 與 Applicative ,它們跟 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 Int 、Maybe 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 something 或 Nothing 後採取進一步動作,例如:
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
那麼,findZipCode 與 findCity 就可以分別改寫如下:
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 ,fmap 在指定對應的函式之後,就可以將 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 來做這些事:
因為已經有了 map 函式,因此要 List 實現 Functor 的行為,只要令 fmap 為 map 就可以了:
instance Functor [] where
fmap = map
雖然你應該很熟悉 List 的 map 在做些什麼,不過還是來照樣造句一下,這有助於瞭解 Functor 定義了什麼行為,對於 List Functor ,fmap 可以在指定對應的函式之後,將 [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 呢?也就是從 getLine 到 result ,型態是 IO String 到 IO 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 a 至 f b ,那 Either 可以是個 Functor 嗎?一個 Either a b 對應至 Either c d ,顯然不符合 Functor 的定義,不過實際上,一個 Right 40 的型態是 Either a Integer ,有沒有需求可以是 Right 40 對應至 Right "Justin" 呢?你可以試著讓 Either a 實現 Functor 的行為!
後續 >> Haskell Tutorial(26)Functor 的 fmap 行為
|