提升、返回和变压器类型构造函数



一年多来,我一直在大量使用liftreturn和构造函数,如EitherTReaderT等。 我读过现实世界的Haskell,Learn You a Haskell,几乎每个monad教程,并尝试编写自己的教程。 然而,我一直对这三个操作感到困惑。 每当我编写新代码时,我都会尝试找出使用这三者中的哪一个,并且几乎总是需要花费我一个小时或更长时间才能完成特定代码块中的第一个函数。

这三者的直观理解是什么? 简单的类型是不够的,因为在所有三种情况下,我都可以立即向您背诵类型。 在所有标准单元变压器中保持一致的这些功能的含义是什么?

(不幸的是,如果你用数学术语回答,我仍然不会理解你。 虽然我可以编写代码来解决数学问题,并且可以根据我看到的代码设置时间复杂度,但在尝试使用Haskell多年之后,我无法将数学术语与编程术语联系起来。

  • return采用纯粹的计算并将其转换为声称具有一些monad-y副作用的计算,但没有。
  • lift采用具有一些副作用的计算,并添加更多。
  • EitherTReaderT等等,采用一个已经具有你感兴趣的所有副作用的计算,并"以不同的方式拼写它们"——例如,在你的状态被拼写为返回更新值的函数之前,它现在被拼写为一个State(T)的计算。

假设你有一个计算。用像Haskell这样的懒惰语言,你会写

comp1 :: a

并且知道此计算将根据请求执行并生成类型为 a 的值。


假设您有一个类似的计算,但除了计算类型 a 的值之外,它可能会由于某种原因而"失败"。例如,a可能是Integer,如果此计算被零除,则此计算将"失败"。我们现在把它写成

comp2 :: Maybe a

其中Maybe构造函数"标记"a以指示失败。


假设我们有一个与以前类似的计算,但现在我们被允许失败,但也在计算过程中收集日志。"日志收集"被称为Writer因此我们希望用WriterMaybe标记我们的类型。不幸

comp3_bad :: (Writer String) Maybe a

没有任何意义。编写器的定义允许使用单个参数,而不是两个参数。不过,我们可以考虑一下这种组合效应的基本机制是什么——它需要返回一个与日志配对的Maybe......或者,如果计算失败,日志将被丢弃。有两种选择

comp3_1 :: (String, Maybe a)
comp3_2 :: Maybe (String, a)

如果我们解压缩Writer,我们可以看到这些等价于

comp3_1' :: Writer String (Maybe a)
comp3_2' :: Maybe (Writer String a)

这种嵌套模式称为组合。如果你想组合两个单子的效果,那么你想组成它们。对于一些monads来说,这直接起作用,尽管它有点麻烦。

不幸的是,一些单子一旦组成就开始违反单子定律。它们仍然可以"堆叠",但不能以正常方式。因此,我们允许每种类型通过创建转换器版本来确定其堆叠方法 <monad>T .

newtype WriterT w m a = WriterT { runWriterT :: m (w, a) }
newtype MaybeT m a    = MaybeT { runMaybeT :: m (Maybe a) }
-- note that
WriterT String Maybe a == Maybe (String, a)
MaybeT (Writer String) a == (String, Maybe a)

这些组合的单子堆栈称为单子变压器堆栈,它们允许您分层组装副作用。


那么,如果我们有两个不同但相似的堆栈,我们想一起使用,会发生什么。例如,我们可以将Maybe视为一个monad...或单层变压器堆栈。相比之下,WriterT String Maybe 这是一个两层的单变压器堆栈,其底部是 Maybe .

这两个堆栈非常相似,但我们不能将计算从一个传输到另一个堆栈。或者更确切地说,我们可以,但这很烦人

transport :: Maybe a -> WriterT String Maybe a
transport Nothing  = WriterT Nothing
transport (Just a) = WriterT (Just ("", a))

这种transport形成了一个通用模式,我们在堆栈上"添加另一层"。这种常规模式称为lift

lift :: Maybe a -> WriterT String Maybe a

或者,以多态方式编写,我们看到额外的层t被预置。

lift :: MonadTrans t => m a -> t m a

最后,我们已经从一开始的纯计算走了很长一段路

comp1 :: a

并证明了我们可以将简单的变压器堆栈lift成更复杂的变压器堆栈。我们是否可以认为comp1生活在最简单的变压器堆栈中 - 空堆栈?

事实证明,这实际上是一个非常有效的观点。我们甚至可以将comp1"提升"到更复杂的变压器堆栈中......但术语略有变化。

return :: Monad m => a -> m a

因此,将return视为将纯计算提升为基本 monad 是有效的。这甚至是monads的基本原则——它们可以在其中嵌入纯计算。

最新更新