单子变换及其自动升格
上一篇我们提到了State单子可以在不借助全局变量的情况下传递状态,本质上状态s
作为local变量在函数调用链中传递。而State单子通过(>>=) :: State s a -> (a -> State s b) -> State s b
提供了下述关键功能,
-
State s a
及包裹在其中的s -> (a, s)
函数实现了状态的提取(和转换),并且取出当前被包装的值a
; - 把
a
交给用户定义的(a -> State s b)
函数。这个函数生成的也是State单子,并且将新的状态转换函数s -> (a, s)
包裹了进去(这里状态转换并没有执行,而只是作为一个函数存在)。
State单子 -> 状态提取/转换 -> 值的提取 -> 给定状态和值,产生新的状态转换函数 -> State单子
上面的过程首尾衔接,形成了一个连续的调用链,而s
和a
在这个调用链中作为local变量传递,消除了全局可变性。本质上,这和以下的函数调用没有区别,
(s1, a1) = func1(s0, a0)
(s2, a2) = func2(s1, a1)
...
只是单子通过>>=
提供了自动对s
和a
进行解包和打包的机制,而用户只需要专注实现a -> (s -> (b, s))
(本质上也就是上面的func1
和func2
)就可以了。
如果整个调用链中都是State单子,那自然没有问题。但如果要在State单子的调用链中插入其他操作,比如读取IO,怎么办呢?这就要用到下面要讲的单子变换。
单子变换
回忆之前State单子的定义是
newtype State s a = State { runState :: s -> (a, s) }
-- (>>=) :: State s a -> (a -> State s b) -> State s b
在Haskell中,IO操作总是被包裹成一个单子。例如函数getLine :: IO String
从输入中读取一个字符串,并且返回单子IO String
。如果要在State调用链中插入IO操作,很自然的想法便是将a
包裹到IO单子里去,
newtype StateIO s IO a = StateIO { runStateIO :: s -> IO (a, s) }
-- (>>=) :: StateIO s IO a -> (a -> StateIO s IO b) -> State s IO b
注意上面的>>=
定义中,解包所得仍然是a
;或者说,用户提供的值处理函数,所处理的仍然是a
。
如此一来,在用户定义的(a -> State s IO b)
函数中就可以操作IO了,同时结果中,值也被包裹在了IO单子里。本质上,StateIO
单子包含了状态s
和值IO a
。
为了方便编程,通常还会提供一个lift
函数用于把IO单子提升为StateIO。如此就可以写这样的代码了,
lift getLine >>= \input ->
...
-- 或在Haskell中等价于,
do
input <- lift getLine
...
把上面讨论的内容一般化,我们可以把IO单子替换为任意别的单子m,得到StateT,
newtype StateT s m a = State { runStateT :: s -> m (a, s) }
instance (Monad m) => Monad (StateT s m) where
m >>= k = StateT $ \s -> do
(a, s') <- runStateT m s
runStateT (k a) s'
注意上面do语句中解包所得的是m
的包裹(a, s')
,因为此时单子的上下文是m
。
单子变换(lift)也可以用一个通用的类型类来实现,
class MonadTrans t where
lift :: Monad m => m a -> t m a
单子变换的自动升格
使用lift的一个问题是,如果存在嵌套的单子变换,那每一处都lift会让代码变得很复杂。例如IO (Int, Int) -> StateT Int (RandT g IO) (Int, Int)
(读作StateT s m a
)就可能需要两次lift:一次到RandT,再一次到StateT。
为了解决这个问题,Haskell提供了MonadRandom,MonadIO,MonadState等模块。例如,除了RandT g m
是MonadRandom之外,模块里还声明了,一切包括了MonadRandom的单子栈,也是MonadRandom类型,
instance (MonadRandom m) => MonadRandom (IdentityT m) where
getRandom = lift getRandom
...
instance (MonadRandom m) => MonadRandom (StateT m) where
getRandom = lift getRandom
...
...
如此,由于RandT g IO
是MonadRandom,按照上面的定义,StateT Int (RandT g IO) (Int, Int)
自动也成了MonadRandom了,就可以用到MonadRandom中的方法(例如uniform)了。
可见,为了能在程序中方便地串联Monad,Haskell还是做了很多脏活,把不同的单子类型统一到同一类中,并且试图用各种语法糖把这些工作隐藏起来。Monad在形式上固然优美,实际使用中却也难免一些额外的繁复工作。
Enjoy Reading This Article?
Here are some more articles you might like to read next: