Haskell/Monad transformers
我們已經學習了Monad能夠如何簡便對於 IO, Maybe, 列表, 和 State 的處理.
每一個Monad都是如此的有用且用途廣泛, 我們自然會尋求將幾個Monad的特性組合起來的方法. 比如, 也許我們可以定義一個既能處理I/O操作, 又能使用 Maybe 提供的異常處理的函數. 雖然這可以用形如 IO (Maybe a) 的函數實現, 但是在這種方式下我們將被迫在 IO 的 do 代碼塊中手工模式匹配提取值, 而避免寫出這種繁瑣而無謂的代碼卻恰恰是 Maybe Monad存在的原因.
於是我們發明了 monad transformers: 它們能夠將幾種Monad的特性融合起來, 同時不失去使用Monad的靈活性.
密碼驗證
[編輯]我們先來看一看這個幾乎每一個IT從業人員都會遇到的實際問題: 讓用戶設置足夠強的密碼. 一種方案是: 強迫用戶輸入大於一定長度, 且滿足各種惱人要求的密碼 (例如包含大寫字母, 數字, 字符, 等等).
這是一個用於從用戶處獲取密碼的Haskell函數:
getPassphrase :: IO (Maybe String)
getPassphrase = do s <- getLine
if isValid s then return $ Just s
else return Nothing
-- 我们可以要求密码满足任何我们想要的条件
isValid :: String -> Bool
isValid s = length s >= 8
&& any isAlpha s
&& any isNumber s
&& any isPunctuation s
首先, getPassphrase 是一個 IO Monad, 因為它需要從用戶處獲得輸入. 我們還使用了 Maybe, 因為若密碼沒有通過 isValid 的建議, 我們決定返回 Nothing. 需要注意的是, 我們在這裡並沒有使用到 Maybe 作為Monad的特性: do 代碼塊的類型是 IO monad, 我們只是恰巧 return 了一個 Maybe 類型的值罷了.
Monad transformers 不僅僅使 getPassphrase 的實現變簡單了, 而且還能夠簡化幾乎所有運用了多個monad的代碼. 這是不使用Monad transformers的程序:
askPassphrase :: IO ()
askPassphrase = do putStrLn "输入新密码:"
maybe_value <- getPassphrase
if isJust maybe_value
then do putStrLn "储存中..." -- 假装存在数据库操作
else putStrLn "密码无效"
這段代碼單獨使用了一行來獲得 maybe_value, 然後又手工對它進行驗證.
如果使用 monad transformers, 我們將可以把獲得輸入和驗證這兩個步驟合二為一 — 我們將不再需要模式匹配或者等價的 isJust. 在我們的簡單例子中, 或許 monad transformers 只作出了微小的改進, 但是當問題規模進一步擴大時, 它們將發揮巨大的作用.
一個簡單的 monad transformer: MaybeT
[編輯]為了簡化 getPassphrase 以及所有使用到它的函數, 我們定義一個賦予 IO monad 一些 Maybe monad 特性的 monad transformer; 我們將其命名為 MaybeT. 一般來說, monad transformers 的名字都會以 "T" 結尾, 而之前的部分 (例如, 在本例中是"Maybe") 表示它所提供的特性.
MaybeT 是一個包裝 (wrap) 了 m (Maybe a) 的類型, 其中 m 可以是任何Monad (在本例中即為 IO):
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
這裡的 datatype 聲明定義了 MaybeT, 一個被 m 參數化的類型構造子 (type constructor), 以及一個值構造函數 (value constructor), 同樣被命名為 MaybeT, 以及一個作用為簡便的內部值訪問的函數 runMaybeT.
Monad transformers 的關鍵在於 他們本身也是monads; 因此我們需要寫出 MaybeT m 的 Monad 類型類實例:
instance Monad m => Monad (MaybeT m) where
return = MaybeT . return . Just
首先, 我們先用 Just 將值裝入最內層的 Maybe 中, 然後用 return 將前述 Maybe 裝入 m monad里, 最後再用 MaybeT 將整個值包裝起來.
我們也可以這樣實現 (雖然是不是增加了可讀性仍然有待商榷) return = MaybeT . return . return.
如同在所有monad中一樣, (>>=) 也是 transformer 的核心.
-- 特化为 MaybeT m 的 (>>=) 函数类型签名
(>>=) :: MaybeT m a -> (a -> MaybeT m b) -> MaybeT m b
x >>= f = MaybeT $ do maybe_value <- runMaybeT x
case maybe_value of
Nothing -> return Nothing
Just value -> runMaybeT $ f value
我們從 do 代碼塊的第一行開始解釋:
- 首先,
runMaybeT從x中取出m (Maybe a). 我們由此可知整個do代碼塊的類型為 monadm. - 第一行稍後,
<-從上述值中取出了Maybe a. case語句對maybe_value進行檢測:- 若其為
Nothing, 我們將Nothing返回至m當中; - 若其為
Just, 我們將函數f應用到其中的value上. 因為f的返回值類型為MaybeT m b, 我們需要再次使用runMaybeT來將其返回值提取回mmonad 中.
- 若其為
- 最後,
do代碼塊的類型為m (Maybe b); 因此我們用MaybeT值構造函數將它包裝進MaybeT中.
這咋看來獲取有些複雜; 但是若剔除大量的包裝和解包裝, 我們的代碼和 Maybe monad 的實現其實很像:
-- Maybe monad 的 (>>=)
maybe_value >>= f = case maybe_value of
Nothing -> Nothing
Just value -> f value
我們在 do 代碼塊中仍然能夠使用 runMaybeT, 為何要在前者上應用 MaybeT 值構造函數呢? 這是因為我們的 do 代碼塊必須是 m monad, 而不是 MaybeT m monad, 因為後者此時還沒有定義 (>>=). (回想一下 do 語法糖是如何工作的)
| 註解
在 |
理論上, 這就是我們所需的全部了; 但是, 為 MaybeT 實現幾個其他類型類的 instance 又何妨呢?
instance Monad m => MonadPlus (MaybeT m) where
mzero = MaybeT $ return Nothing
mplus x y = MaybeT $ do maybe_value <- runMaybeT x
case maybe_value of
Nothing -> runMaybeT y
Just _ -> return maybe_value
instance MonadTrans MaybeT where
lift = MaybeT . (liftM Just)
MonadTrans monad 實現了 lift 函數, 由此我們可以在 MaybeT m monad 的 do 代碼塊中使用m monad 的值. 至於 MonadPlus, 因為 Maybe 有著這個類型類的 instance, MaybeT 也應該有著對應的 instance.
應用在密碼驗證的例子中
[編輯]現在, 前例的密碼管理可以被改寫為:
getValidPassphrase :: MaybeT IO String
getValidPassphrase = do s <- lift getLine
guard (isValid s) -- MonadPlus 类型类使我们能够使用 guard.
return s
askPassphrase :: MaybeT IO ()
askPassphrase = do lift $ putStrLn "输入新密码:"
value <- getValidPassphrase
lift $ putStrLn "储存中..."
這段代碼簡潔多了, 特別是 askPassphrase 函數. 最重要的是, 有著 (>>=) 為我們代勞, 我們不再需要手動檢查結果是 Nothing 和 Just 中的哪一個了.
我們使用了 lift 函數以在 MaybeT IO monad 中使用 getLine 和 putStrLn; 因為 MaybeT IO 有著 MonadPlus 類型類的 instance, guard 可以為我們檢查代碼的合法性. 在密碼不合法時其將返回 mzero (即 IO Nothing).
碰巧, 有了 MonadPlus, 我們可以優雅地不停要求用戶輸入一個合法的密碼:
askPassword :: MaybeT IO ()
askPassword = do lift $ putStrLn "输入新密码:"
value <- msum $ repeat getValidPassphrase
lift $ putStrLn "储存中..."
泛濫的 transformer
[編輯]transformers 包提供了許多包含常見 monad 的 transformer 版本的模塊 (例如 Template:Haskell lib 模塊提供了 MaybeT). 它們和對應的非 transformer 版本 monad 是兼容的; 換句話說, 除去一些和另一個 monad 交互所用到的包裝和解包裝 , 它們的實現基本上是一致的. 從今往後, 我們稱 transformer monad 所基於的非 transformer 版本的 monad 為 基層 monad (例如 MaybeT 中的 Maybe); 以及稱應用在 transformer 上的 monad 為 內層 monad (例如 MaybeT IO 中的 IO).
我們隨便舉一個例子, ReaderT Env IO String. 這是一個能夠從某個外部環境讀取 Env 類型的值 (並且和 Reader, 也就是基 monad, 使用方法相同) 並且能夠進行一些I/O, 最後返回一個 String 類型的值的 monad. 由於這個 transformer 的 (>>=) 和 return 語義和基 monad 相同, 一個 ReaderT Env IO String 類型的 do 語句塊從外部看來和另一個 Reader 類型的 do 語句塊並無二致, 除了我們需要用 lift 來使用 IO monad.
類型戲法
[編輯]我們已經了解到, MaybeT 的類型構造子實際上是一個對內層 monad 中的 Maybe 值的包裝. 因此, 用於取出內部值的函數 runMaybeT 返回給我們一個類型為 m (Maybe a) 的值 - 也就是一個由內層 monad 包裝著的基 monad 值. 類似的, 對於分別基於列表和 Either 構建的 ListT 和 ExceptT transformer:
runListT :: ListT m a -> m [a]
and
runExceptT :: ExceptT e m a -> m (Either e a)
然而並不是所有的 transformer 都和它們的基 monad 有著類似的關係. 不同於上面兩例所給出的基 monad, Writer, Reader, State, 以及 Cont monad 既沒有多個值構造子, 也沒有接收多個參數的值構造函數. 因此, 它們提供了形如 run... 的函數以作為解包裝函數, 而它們的 transformer 則提供形如 run...T 的函數. 下表給出了這些 monad 的 run... 和 run...T 函數的類型, 而這些類型事實上就是包裝在基 monad 和對應的 transformer 中的那些. [2]
| 基 Monad | Transformer | 原始類型 (被基 monad 包裝的類型) |
複合類型 (被 transformer 包裝的類型) |
|---|---|---|---|
| Writer | WriterT | (a, w) |
m (a, w)
|
| Reader | ReaderT | r -> a |
r -> m a
|
| State | StateT | s -> (a, s) |
s -> m (a, s)
|
| Cont | ContT | (a -> r) -> r |
(a -> m r) -> m r
|
我們可以注意到, 基 monad 並沒有出現在複合的類型中. 這些 monad 並沒有一個像 Maybe 或列表那樣的有意義的構造函數, 因此我們選擇不在 transformer monad 中保留基 monad. 值得注意的是, 在後三個例子中我們包裝的值是函數. 拿 StateT 作例子, 將原有的表示狀態轉換的 s -> (a, s) 函數變為 s -> m (a, s) 的形式; 然而我們只將函數的返回值 (而不是整個函數) 包裝進內層 monad 中. ReaderT 也與此差不多. ContT 卻與它們不同: 由於 Cont (表示延續的 monad) 的性質, 被包裝的函數和作這個函數參數的函數返回值必須相同, 因此 transformer 將兩個值都包裝入內層 monad 中. 遺憾的是, 並沒有一種能將普通的 monad 轉換成 transformer 版本的萬靈藥; 每一種 transformer 的實現都和基 monad 的行為有關.
Lifting
[編輯]我們將仔細研究 lift 函數: 它是應用 monad transformers 的關鍵. 首先, 我們需要對它的名字 "lift" 做一些澄清. 在理解_Monad中, 我們已經學習過一個名字類似的函數 liftM. 我們了解到, 它實際上是 monad 版本的 fmap:
liftM :: Monad m => (a -> b) -> m a -> m b
liftM 將一個類型為 (a -> b) 的函數應用到一個 m monad 內的值上. 我們也可以將它視為只有一個參數:
liftM :: Monad m => (a -> b) -> (m a -> m b)
liftM 將一個普通的函數轉換為在 m monad 上運作的函數. 我們用"提升(lifting)"表示將一樣東西帶至另一樣東西中 — 在前例中, 我們將一個函數提升到了 m monad 中.
有了 liftM, 我們不需要用 do 代碼塊或類似的技巧就能夠把尋常的函數應用在 monad 上了:
| do notation | liftM |
|---|---|
do x <- monadicValue
return (f x)
|
liftM f monadicValue
|
類似的, lift 函數在 transformer 上起到了一個相似的作用. 它將內層 monad 中的計算帶至複合的 monad (即 transformer monad) 中. 這使得我們能夠輕易地在複合 monad 中插入一個內層 monad 的運算.
lift 是 MonadTrans 類型類的唯一一個函數, 參見 Template:Haskell lib. 所有的 transformer 都有 MonadTrans 的 instance, 因此我們能夠在任何一個 transformer 上使用 lift.
class MonadTrans t where
lift :: (Monad m) => m a -> t m a
lift 存在一個 IO 的變種, 名叫 liftIO; 這是 MonadIO 類型類的唯一一個函數, 參見 Template:Haskell lib.
class (Monad m) => MonadIO m where
liftIO :: IO a -> m a
當多個 transformer 被組合在一起時, liftIO 能夠帶來一些便利. 在這種情況下, IO 永遠是最內層的 monad (因為不存在 IOT transformer), 因此一般來說我們需要多次使用 lift 以將 IO 中的值從底層提升至複合 monad 中. 定義了 liftIO instance 的類型被設計成能夠使用 liftIO 將 IO 從任意深的內層一次性提升到 transformer monad 中.
實現 lift
[編輯]lift 並不是很複雜. 以 MaybeT transformer 為例:
instance MonadTrans MaybeT where
lift m = MaybeT (liftM Just m)
我們從接收到一個內層 monad 中的值的參數開始. 我們使用 liftM (或者 fmap, 因為所有Monad都首先是Functor) 和 Just 值構造函數來將基 monad 插入內層 monad 中, 以從 m a 轉化為 m (Maybe a)). 最後, 我們使用 MaybeT 值構造函數將三明治包裹起來. 值得注意的是, 此例中 liftM 工作於內層 monad 中, 如同我們之前看到的 MaybeT 的 (>>=) 實現中的 do 代碼塊一樣.
| 練習 |
|---|
|
實現 transformers
[編輯]State transformer
[編輯]作為一個額外的例子, 我們將試著實現 StateT. 請確認自己了解 State 再繼續. [3]
正如同 State monad 有著 newtype State s a = State { runState :: (s -> (a,s)) } 的定義一樣, StateT transformer 的定義為:
newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }
StateT s m 有著如下的 Monad instance, 旁邊用基 monad 作為對比:
| State | StateT |
|---|---|
newtype State s a =
State { runState :: (s -> (a,s)) }
instance Monad (State s) where
return a = State $ \s -> (a,s)
(State x) >>= f = State $ \s ->
let (v,s') = x s
in runState (f v) s'
|
newtype StateT s m a =
StateT { runStateT :: (s -> m (a,s)) }
instance (Monad m) => Monad (StateT s m) where
return a = StateT $ \s -> return (a,s)
(StateT x) >>= f = StateT $ \s -> do
(v,s') <- x s -- 取得新的值和状态
runStateT (f v) s' -- 将它们传递给 f
|
我們的 return 實現使用了內層 monad 的 return 函數. (>>=) 則使用一個 do 代碼塊來在內層 monad 中進行計算.
| 註解
現在我們能夠解釋, 為什麼在 State monad 中存在手工定義的 |
為了將 StateT s m monad 同 State monad 一般使用, 我們自然需要核心的 get 和 put 函數. 這裡我們將使用 mtl 的代碼風格. mtl 不僅僅提供了 monad transformers 的定義, 還提供了定義了常見 monad 的關鍵操作的類型類. 例如, Template:Haskell lib 包所提供的 MonadState 類型類, 定義了 get 和 put 函數:
instance (Monad m) => MonadState s (StateT s m) where
get = StateT $ \s -> return (s,s)
put s = StateT $ \_ -> return ((),s)
| 註解
|
存在為其他 transformer 包裝著的 state monad 所定義的 MonadState instance, 例如 MonadState s m => MonadState s (MaybeT m). 這些 instance 使得我們不再需要寫出 lift 來使用 get and put, 因為複合 monad 的 MonadState instance 為我們代勞了.
我們還可以為複合 monad 實現內層 monad 所擁有的一些類型類 instance. 例如, 所有包裝了 StateT (其擁有 MonadPlus instance) 的複合 monad 也同樣能夠擁有 MonadPlus 的 instance:
instance (MonadPlus m) => MonadPlus (StateT s m) where
mzero = StateT $ \_ -> mzero
(StateT x1) `mplus` (StateT x2) = StateT $ \s -> (x1 s) `mplus` (x2 s)
mzero 和 mplus 的實現符合我們的直覺; 它們將實際的操作下推給內層 monad 來完成.
最後, monad transformer 必須有 MonadTrans 的 instance, 不然我們無法使用 lift:
instance MonadTrans (StateT s) where
lift c = StateT $ \s -> c >>= (\x -> return (x,s))
lift 函數返回一個包裝在 StateT 中的函數, 其接受一個表示狀態的值 s 作為參數, 返回一個函數, 這個函數接受一個內層 monad 中的值, 並將它通過 (>>=) 傳遞給一個將其和狀態打包成一個在內層 monad 中的二元組的函數. 舉個例子, 當我們在 StateT transformer 中使用列表時, 一個返回列表的函數 (即一個在列表 monad 中的計算) 在被提升為 StateT s []後, 將變成一個返回 StateT (s -> [(a,s)]) 的函數. 換句話說, 這個函數用初始狀態產生了多個 (值, 新狀態) 的二元組. 我們將 StateT 中的計算 "fork" 了, 為被提升的函數返回的列表中的每一個值創建了一個計算分支. 當然了, 將 StateT 和不同的 monad 組合將產生不同效果的 lift 函數.
| 練習 |
|---|
|
致謝
[編輯]本章摘取了 All About Monads, 已取得作者 Jeff Newbern 的授權.
| Monad transformers |
| 習題解答 |
| Monads |
|
理解 Monad >> 高級 Monad >> Monad 進階 >> MonadPlus >> Monadic parser combinators >> Monad transformers >> Monad 實務 |
| Haskell |
|
Haskell基礎
>> 初級Haskell
>> Haskell進階
>> Monads
|