Haskell Tutorial(29)一個型態的 newtype << 前情
假設你現在有 findOrder 、findCustomer 、findAddress 等函式:
findOrder :: String -> Maybe Order
findOrder number = -- 一些程式碼
findCustomer :: Order -> Maybe Customer
findCustomer order = -- 一些程式碼
findAddress :: Customer -> Maybe Address
findCustomer customer = -- 一些程式碼
這三個函式分別代表,你可以從訂單號碼查詢訂單資訊(Order ),從訂單資訊中查詢客戶資訊(Customer ),從客戶資訊中查詢位址資訊(Address ),函式的傳回值都是 Maybe Something ,表示可能有也可能沒有結果。
那麼,如果你有個訂單號碼,想要一路查出位址位址資訊,會怎麼寫呢?
address =
case findOrder "X1234" of
Nothing -> Nothing
Just order -> case findCustomer order of
Nothing -> Nothing
Just customer -> findAddress customer
重複、難以閱讀等問題顯而易見,出現了巢狀的結構,如果想要一路查找出更多資訊,情況就會更糟,你可能會想到,在取得 Maybe Order 之後,接下來是取得 Maybe Customer ,然後是取得 Maybe Address ,這讓我們會回想起 Functor ,也想到了〈Haskell Tutorial(25)可被映射盒中物的 Functor〉中談到類似的範例,也就是從訂單號碼查找郵遞區號與城市資訊的例子,fmap 可以解決這個問題嗎?
fmap 的型態是 (a -> b) -> f a -> f b ,因為 findOrder 、findCustomer 、findAddress 等函式的型態,都是 a -> Maybe b ,我們沒辦法直接將 findOrder 、findCustomer 、findAddress 直接當作 fmap 的第一個引數,就算你勉強寫出了以下的程式,Callback hell 只會令情況更糟:
address = case fmap (\order ->
fmap (\customer -> findAddress customer) (findCustomer order))
(findOrder "X1234") of Nothing -> Nothing
Just (Just (Just addr)) -> Just addr
Maybe Monad
不過,fmap 給了點啟發,我們需要類似的版本,不過要能直接接受 a -> Maybe b 函式,仔細觀察一開始判斷值存在與否的巢狀結構,每一層都是這樣的:
case f something1 of
Nothing -> Nothing
Just something2 -> case f something2 of -- 重複的結構
我們可以寫個 may 函式,讓 f 當做引數傳入:
may :: Maybe a -> (a -> Maybe b) -> Maybe b
may Nothing _ = Nothing
may (Just something) f = f something
這麼一來,原先的 address 就可以寫成:
address = findOrder "X1234" `may` findCustomer `may` findAddress
實際上,像 may :: Maybe a -> (a -> Maybe b) -> Maybe b 這樣的行為,Haskell 在 Control.Monad 模組中,使用了 Monad 這個 Typeclass 來定義:
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
x >> y = x >>= \_ -> y
fail :: String -> m a
fail msg = error msg
需要實現的是 return 與 >>= 函式,從 >>= 函式型態可以看出來,這是比上頭 may 更通用的型態,return 用來將指定的值,放到情境 m 之中,唔!談到情境,從〈Haskell Tutorial(27)可直接函式套用的 Applicative〉就一直試著用白話來解釋,就 Monad 來說,因為 >>= 要處理 m a ,如果有個 a ,自然就需要用 return 來得到 m a !
你也許猜到了,Maybe 就實現了 Monad 的行為:
instance Monad Maybe where
return x = Just x
Nothing >>= _ = Nothing
(Just something) >>= f = f something
因此,將方才的需求,你可以直接如下撰寫:
address = findOrder "X1234" >>= findCustomer >>= findAddress
你可以一路進行下去,解決掉原先會形成巢狀結構的問題,閱讀起來也很輕鬆,就是找出訂單、找出客戶、找出位址,有結果的就是 Just something ,沒結果的話就是 Nothing 。
List Monad
現在來看另一個需求,如果有一串訂單(Order ),每個訂單上有項目(Item ),你想取得全部訂單上全部項目,那麼可以先使用 fmap findItems orders ,其中 findItems 是型態 Order -> [Item] 的函式,因此 fmap findItems orders 會得到 [[Item]] ,最後再使用 concat 將元素串起來成為 [Item] ,就可以得到結果。
如果要進一步使用 findPremiums :: Item -> [Premium] 從 [Item] 取得每個項目的贈品(Premium )清單呢?那就是 concat (fmap findPremiums items) 啦!顯然地,出現重複的結構了。
仔細觀察 findItems 與 findPremiums ,一個是 Order -> [Item] ,一個是 Item -> [Premium] ,嗯?a -> [b] ?這不就是 a -> m b 的模式嗎?那麼,List 可以是個 Monad 嗎?確實是的:
instance Monad [] where
return x = [x]
xs >>= f = concat (map f xs)
fail _ = []
因此,對於以上需求,如果想從一串訂單查得一串贈品,可以直接使用 orders >>= findItems >>= findPremiums ,最後得到一個 [Item] 。
IO Monad
回顧一下〈Haskell Tutorial(1)哈囉!世界!〉,重新寫個「哈囉!世界!」吧!
main = do
name <- getLine
putStrLn ("哈囉, " ++ name)
getLine 的傳回型態是 IO String ,你取得其中的 String ,然後執行 putStrLn ,得到一個 IO () ,也就是說你做了一個從 String -> IO () 的動作,將一開始 IO String 對應至 IO () ,那麼,IO 可以是個 Monad 嗎?是的!上面的程式也可以這麼寫:
main =
(getLine >>= (\name -> putStrLn ("Hello, " ++ name)))
這是一個不使用 do 的版本,記得嗎?〈Haskell Tutorial(20)初探 IO 型態〉中談到「可以先將 do 理解為,可將一連串會傳回 IO 的函式串接在一起,成為一個更大的 IO ,具體來說,do 定義了一個函式呼叫,而這個被呼叫的函式中包括了數個會傳回 IO 的函式,而 do 定義的函式在呼叫過後傳回的結果型態,取決於其包括的函式中,最後一個函式傳回的型態」!
實際上,do 就只是讓你不用寫一長串的 Lambda 罷了,使用 do 最後也是轉為 >>= 來算,那麼 do 看來不是只能作用在 IO 上,而是可以作用在 Monad 上囉?是的!下一個主題中,我們會看到 do 可以作用在 Maybe、List 等 Monad 上,當然,與之搭配的 <- 也是!
Monad 定律
最後,如同 Functor 與 Applicative 在實作時,都有其要遵守的定律,Monad 也有其要遵守的定律,有機會的話可以思考看看,遵守以下這些定律的意義:
return a >>= k == k a
m >>= return == m
m >>= (\x -> k x >>= h) == (m >>= k) >>= h
後續 >> Haskell Tutorial(31)do 區塊與 <- 綁定
|