之前提過很多次 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
的行為!