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
來將其返回值提取回m
monad 中.
- 若其為
- 最後,
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
|