请问您今天要来点Monad Transformer吗?

最近在思考关于Hakell的学习曲线的问题。Haskell的入门难已经广为人知了吧… 虽然我从一窍不通到现在缺失经历了很长一段时间的挣扎,不过我还是很好奇这个难度的实体究竟是什么。如果说Monad对新手来说是第一道难以逾越的门槛的话,那么搞清楚后面几道难关是什么队学习总是会有些帮助。粗略地看起来,Haskell中的难度大概有这么几个方面:

  • 副作用: Monad -> Monad Transformer -> Comonad -> Continuation Monad -> Free Monad -> ….
  • 并发模型: MVar -> STM -> …
  • 并行模型: Eval Monad -> Par Monad -> …
  • 运行时特性: Template Haskell -> Quasi Qoutation -> …
  • 类型系统: Type Family -> …

然而就算搞懂了以上这些非常抽象的概念之后,Haskell也才刚刚只是算入门而已(更不要再算上GHC那些进化的比我学习都快的特性了)。嘛,一门能搞一辈子的语言,不也是挺好的嘛。

好了那么言归正传,今天就来谈一谈Monad Transformer这个东西。

为什么需要Monad Transformer?

我们知道Monad是隐式保存了额外状态信息的计算过程,那么既然有了Monad,为什么还需要Monad Transformer呢?

解释起来很简单。每一种Monad都有其独特的功能,比如Reader Monad提供了类似全局(不)变量的功能,Writer Monad提供了计算同时记录信息的功能,State Monad提供了可变状态,IO Monad提供了副作用。这些Monad各有各的功能,但是,我要是想并发地执行多个线程,它们共享一个配置文件,其中每条线程读取硬盘上的一些文件,并能在读取失败的时候保存日志,要怎么办?这些Monad各司其职这很好,但我们需要一种手段把它们组合起来,于是这里就用到了Monad Transformer

好吧… 但Monad Transformer究竟是什么?

本质上说,Monad Transformer就是一个个Monad堆起来的栈。可以想象一个洋葱,最里面的一层是原始的Monad,每在外面增加一层就会增加一层Monad的功能。比较有意思的一点是,Monad Transformer依然可以被当作Monad Transformer的参数,也就是说可以组合出任意复杂的Monad Transformer来(类比把Monad堆得任意高)。

Shut up and show me the code!

考虑上面说的那个例子。首先我们需要一些IO操作,所以在最底层我们放置IO Monad

1
2
foo :: IO String
foo = readFile "foo.txt"

硬编码总不是一件好事,对吧?我们来把要读取得文件名写成配置文件:

1
2
3
4
newtype Config = Config { fileName :: String } deriving Show

defaultConfig :: Reader Config Config
defaultConfig = return $ Config { fileName = "foo.txt" }

现在为了在IO Monad中使用这个配置文件,我们就要用到ReaderT了:

1
2
3
4
5
-- newtype ReaderT r m a
foo :: ReaderT Config IO String
foo = do
name <- ask
liftIO $ readFile name

关于liftIO的事情之后再讨论,现在可以这样使用它:

1
2
3
-- runReaderT :: ReaderT r m a -> r -> m a
getFoo :: IO String
getFoo = runReaderT foo defaultConfig

再多一点Monad Transformer!

好吧。现在我们希望foo在做IO操作的时候需要打印日志。这么做似乎没有什么特别的理由,但是现实世界本来就不是讲道理的对吧w。这时候我们就需要用到Writer Monad,同时我们的程序会变成这样:

1
2
3
4
5
foo :: ReaderT Config (WriterT String IO) a
foo = do
name <- ask
liftIO $ readFile name
lift . tell $ "I am reading " ++ name ++ " !"

好吧,lift又是什么?读者可以先自行猜测一下,我们这里先不理会它。实际上我们发现,在使用了Monad Transformer之后,除了类型签名变得不太雅致以外,我们完全不用任何特殊的写法就可以同时获得三个Monad的功能(忽略那些lift的话… 事实上,的确可以忽略),这真是太棒了!

lift!lift!lift!

前面出现了许多奇奇怪怪的lift操作,什么是lift?我们来看一下它的签名:

1
2
3
class MonadTrans (t :: (* -> *) -> * -> *) where
lift :: Monad m => m a -> t m a
-- Defined in ‘Control.Monad.Trans.Class’

这里需要说明的是,如果不加任何提示,默认是会留在最外层的Monad的。比如两个State Monad叠在一起:

1
2
3
4
5
6
7
bar :: StateT Int (State String) (String,Int)
bar = do
modify (+1) -- outer monad
lift $ modify (++ "1") -- inner monad
a <- get
b <- lift get
return (b,a)

这就属于需要显示说明的情况。而不同的Monad叠在一起时,只要内层的Monad实现了外层的接口,就可以不用显示的lift操作。

1
2
3
4
5
ask :: MonadReader r m => m r
-- 注意到类型为m,只需实现MonadReader接口

instance MonadReader r m => MonadReader r (StateT s m)
-- 可以在StateT中直接使用ask

那么IO操作要怎么办?难道我们要一点点lift . lift . lift ...过去吗?由于IO类型没有Transformer,所以Haskell提供了MonadIO类型类,可以直接使用liftIO来提升IO操作。

自定义的Monad Transformer?

这个完全可以再单开一篇文章来讲了… 可以参考Real World Haskell第十八章实现的MaybeT

Monad Transformer的顺序?

在有的情况下,Monad Transformer的顺序是无关紧要的,比如上例的两个State Monad。但在有的情况下,交换顺序可能得到完全错误的结果,这要依使用场景而定。例如如果要交换fooReaderTWriterT的顺序的话,使用execWriterT就会抛弃内层的ReaderT Config IO String结果,只留下String类型的日志。

以上。