最近在思考关于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 | foo :: IO String |
硬编码总不是一件好事,对吧?我们来把要读取得文件名写成配置文件:
1 | newtype Config = Config { fileName :: String } deriving Show |
现在为了在IO Monad中使用这个配置文件,我们就要用到ReaderT了:
1 | -- newtype ReaderT r m a |
关于liftIO的事情之后再讨论,现在可以这样使用它:
1 | -- runReaderT :: ReaderT r m a -> r -> m a |
再多一点Monad Transformer!
好吧。现在我们希望foo在做IO操作的时候需要打印日志。这么做似乎没有什么特别的理由,但是现实世界本来就不是讲道理的对吧w。这时候我们就需要用到Writer Monad,同时我们的程序会变成这样:
1 | foo :: ReaderT Config (WriterT String IO) a |
好吧,lift又是什么?读者可以先自行猜测一下,我们这里先不理会它。实际上我们发现,在使用了Monad Transformer之后,除了类型签名变得不太雅致以外,我们完全不用任何特殊的写法就可以同时获得三个Monad的功能(忽略那些lift的话… 事实上,的确可以忽略),这真是太棒了!
lift!lift!lift!
前面出现了许多奇奇怪怪的lift操作,什么是lift?我们来看一下它的签名:
1 | class MonadTrans (t :: (* -> *) -> * -> *) where |
这里需要说明的是,如果不加任何提示,默认是会留在最外层的Monad的。比如两个State Monad叠在一起:
1 | bar :: StateT Int (State String) (String,Int) |
这就属于需要显示说明的情况。而不同的Monad叠在一起时,只要内层的Monad实现了外层的接口,就可以不用显示的lift操作。
1 | ask :: MonadReader r m => m r |
那么IO操作要怎么办?难道我们要一点点lift . lift . lift ...过去吗?由于IO类型没有Transformer,所以Haskell提供了MonadIO类型类,可以直接使用liftIO来提升IO操作。
自定义的Monad Transformer?
这个完全可以再单开一篇文章来讲了… 可以参考Real World Haskell第十八章实现的MaybeT。
Monad Transformer的顺序?
在有的情况下,Monad Transformer的顺序是无关紧要的,比如上例的两个State Monad。但在有的情况下,交换顺序可能得到完全错误的结果,这要依使用场景而定。例如如果要交换foo中ReaderT和WriterT的顺序的话,使用execWriterT就会抛弃内层的ReaderT Config IO String结果,只留下String类型的日志。
以上。