最近在思考关于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
类型的日志。
以上。