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
|