do 區塊與 <- 綁定

February 12, 2022

在〈解決巢狀運算的 Monad〉最後說到,do<- 綁定其實可以用在 Monad,而不只是 IO,這邊就實際來看看,像 Maybe、list 可以如何與 do<- 一起使用。

Maybe、do 與 <-

假設從某個運算取得了 maybeName::Maybe String,又從另一個運算取得了 maybeBirth::Maybe Int,現在打算將它們進行串接,取得一個字串,你會怎麼做?如果使用模式比對,你得判斷是不是有值,你可能會想到 Maybe 具有 Applicative 行為,它已經實作了有無值的判斷,這是個好主意!你可以使用 (++) <$> something <*> (show <$> another) 來得到想要的結果。

不過,你有更好的做法:

account :: Maybe String -> Maybe Int -> Maybe String
account maybeName maybeBirth = do
    name <- maybeName
    birth <- maybeBirth
    return (name ++ (show birth))

這麼一來就可以如此使用:

ghci> account (Just "Justin") (Just 526)
Just "Justin526"
ghci>

如果不使用 do 區塊與 <- 綁定,就得逐層地寫出 >>= 與 lambda 函式:

account :: Maybe String -> Maybe Int -> Maybe String
account maybeName maybeBirth =
    maybeName >>= (\name -> maybeBirth >>= (\birth -> return (name ++ (show birth))))

括號只是為了讓你便於識別 lambda 函式罷了,來去掉括號並略為排版:

account :: Maybe String -> Maybe Int -> Maybe String
account maybeName maybeBirth =
    maybeName  >>= \name ->
    maybeBirth >>= \birth ->    
    return (name ++ (show birth))

就結論而言,do 區塊就是銜接 Monad 的簡便方式,這也說明了,為什麼 do 區塊最後必須傳回 Monad

List、do 與 <-

將一開始的 account 函式型態宣告拿掉,也就是只寫為:

account mName mBirth = do
    name <- mName
    birth <- mBirth
    return (name ++ (show birth))

由編譯器為你判定型態,你覺得結果會怎樣?

ghci> :t account
account :: (Monad m, Show a) => m [Char] -> m a -> m [Char]
ghci> account ["Justin"] [526]
["Justin526"]
ghci> account ["Justin"] []   
[]
ghci> account [] [526]        
[]
ghci>

實際上,account 只用到了 Monad 定義的 return 方法,判斷為 (Monad m, Show a) => m [Char] -> m a -> m [Char] 是最寬鬆的結果,如此一來,account 不僅可適用 Maybe,也可以適用 list,就像上面示範的,給 account 的 list 只有一個元素時很好理解,就是將唯一的元素綁定到名稱上,如果給 account 的 list 不只有一個元素時會如何呢?

ghci> account ["Justin", "Monica"] [804]
["Justin804","Monica804"]
ghci> account ["Justin", "Monica"] [526, 723]
["Justin526","Justin723","Monica526","Monica723"]
ghci> account ["Justin"] [526, 723]          
["Justin526","Justin723"]
ghci>

難以理解嗎?重新看看不用 do 時會怎麼寫,就應該能知道為什麼對 list 做 do<-,會有這樣的結果:

account mName mBirth =
    mName  >>= \name ->
    mBirth >>= \birth ->    
    return (name ++ (show birth))

我們對 mName 套用 >>=,記得嗎?list 在實作 >>= 的方式是 concat (fmap f xs),也就是說對 mName 套用 >>=,結果就是 concat (fmap f mName),如果 mName 實際上超過一個元素,就會逐一取得並套用 f 後串接起來,後 f 是個 Lambda,它對 mBirth 套用 >>=,如果 mBrith 實際上超過一個元素,就會逐一取得並套用 f 後串接起來,最後就得到與 [name ++ (show birth) | name <- ["Justin", "Monica"], birth <- [526, 723]] 這個 Comprehension 表示式的相同結果。

將以下函式與上頭的函式對照一下,你就更容易記得,對 list 使用 do<- 會是什麼結果了:

account mName mBirth = 
    [name ++ (show birth) | 
     name  <- mName, 
     birth <- mBirth]

也就是說,對 list 使用 do<- 時,將 <- 想成是 Comprehension 表示式的 <- 就是了,實際上,Comprehension 表示式是個語法蜜糖,跟使用 do 一樣,Comprehension 表示式最後也是使用 >>= 來進行運算。

Control.Monad 模組

方才看而,一開始的 account 函式可以讓編譯器自動推斷為 (Monad m, Show a) => m [Char] -> m a -> m [Char],因此,account 也可以適用 IO

ghci> account getLine (return 526)
Justin
"Justin526"
ghci>

這讓我們想起來,在〈來寫些迴圈吧!?〉使用過的一些函式,記得嗎?那些函式是定義在 Control.Monad 模組,實際上它們並不只能用在 IO,像 forM 型態是 Monad m => [a] -> (a -> m b) -> m [b],除了使用在 IO,也可以使用在 Maybe 或 list:

ghci> import Control.Monad
ghci> forM [1, 2, 3, 4] (\x -> Just (x + 10))
Just [11,12,13,14]
ghci> forM [1, 2, 3, 4] (\x -> [x])          
[[1,2,3,4]]
ghci> forM [1, 2, 3, 4] (\x -> [x, x])
[[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4]]
ghci>

分享到 LinkedIn 分享到 Facebook 分享到 Twitter