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

Haskell 提升、返回和变压器类型构造函数,haskell,monad-transformers,Haskell,Monad Transformers,一年多以来,我一直在大量使用lift、return,以及EitherT、ReaderT等构造函数。我读过真实世界的哈斯克尔,向你学习了哈斯克尔,几乎所有的蒙纳德教程,并尝试着写我自己的。然而,我始终对这三项行动感到困惑。每当我编写新代码时,我都会试图找出这三个函数中的哪一个要使用,而对于特定代码块中的第一个函数,我几乎总是需要一个小时或更长的时间 对这三个问题的直观理解是什么?简单的类型是不够的,因为在所有三种情况下,我可以立即背诵给你的类型。在所有标准monad变压器中,这些变压器的作用是一致

一年多以来,我一直在大量使用
lift
return
,以及
EitherT
ReaderT
等构造函数。我读过真实世界的哈斯克尔,向你学习了哈斯克尔,几乎所有的蒙纳德教程,并尝试着写我自己的。然而,我始终对这三项行动感到困惑。每当我编写新代码时,我都会试图找出这三个函数中的哪一个要使用,而对于特定代码块中的第一个函数,我几乎总是需要一个小时或更长的时间

对这三个问题的直观理解是什么?简单的类型是不够的,因为在所有三种情况下,我可以立即背诵给你的类型。在所有标准monad变压器中,这些变压器的作用是一致的,这意味着什么

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

    return
    接受纯计算,并将其转换为声称有一些monad-y副作用的计算,但没有
  • lift
    进行的计算会产生一些副作用,并且会增加更多
  • EitherT
    ReaderT
    ,等等都会进行一种计算,这种计算已经具有您感兴趣的所有副作用,并且“拼写方式不同”——例如,在您的状态被拼写为返回更新值的函数之前,它现在被拼写为
    状态(T)
    -ful计算

假设您有一个计算。你会用哈斯克尔那样的懒散语言来写

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


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

comp2 :: Maybe a
其中
可能
构造函数“标记”了
a
,以指示故障


假设我们有一个与以前类似的计算,但是现在我们被允许失败,但在计算过程中还收集了一个日志。“日志收集”被称为
Writer
,因此我们想用
Writer
以及
Maybe
来标记我们的类型。不幸的是

comp3_bad :: (Writer String) Maybe a
没有任何意义。writer的定义只允许一个参数,而不是两个。我们可以考虑一下这种组合效应的基本机制,尽管它需要返回一个<代码>也许与日志配对…或者,如果计算失败,日志将被丢弃。有两种选择

comp3_1 :: (String, Maybe a)
comp3_2 :: Maybe (String, a)
如果我们打开
编写器
,我们可以看到它们相当于

comp3_1' :: Writer String (Maybe a)
comp3_2' :: Maybe (Writer String a)
这种嵌套模式称为组合。如果你想组合两个单子的效果,那么你想组合它们。对于一些单子来说,这直接起作用,尽管有点麻烦

不幸的是,一些单子一旦组成就开始违反单子定律。它们仍然可以“堆叠”,但不是以正常方式。因此,我们允许每种类型通过创建transformer version
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)
这些由单子组成的堆栈称为单子变压器堆栈,它们允许您在层中组装副作用


那么,如果我们有两个不同但相似的堆栈要一起使用,会发生什么呢。例如,我们可以考虑<代码>也许<代码>是一个单元格…或者是单层的monad转换器堆栈。将其与
WriterT-String-Maybe
进行比较,后者是由两层组成的monad-transformer堆栈,其底部是
Maybe

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

transport :: Maybe a -> WriterT String Maybe a
transport Nothing  = WriterT Nothing
transport (Just a) = WriterT (Just ("", a))
这个
传输
形成了一个通用模式,我们在堆栈上“添加另一层”。这种通用模式称为
lift

lift :: Maybe a -> WriterT String Maybe a
或者,以多态方式编写,我们可以看到额外的层
t
被预先添加

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

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

comp1 :: a
并演示了我们可以将简单的变压器组提升到更复杂的变压器组中。我们可以考虑<代码> COMP1生存在最简单的变压器栈中吗? 事实证明,这是一个非常有效的观点。我们甚至可以将comp1提升到一个更复杂的变压器堆栈中。。。但术语略有变化

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

因此,将
return
看作是将一个纯计算提升到一个基本的monad是正确的。这是单子的基本原理,即使它们可以在其中嵌入纯计算。

如果你知道它们的类型,问题是什么?他们做他们类型的人说他们做的事。我不是在这里刻薄,我真的100%不理解。这就像问什么时候应该使用类型为
Int->Bool
的函数,而不是类型为
String->Bool
的函数。您使用的是与所需类型匹配的类型。我将重点了解
lift
return
,以及独立的各种构造函数。没有任何一条线索能将它们联系在一起,提供比你单独理解每一条线索更深刻的见解。对于如何理解每一个问题,这实际上应该是三个独立的问题。本质上,
return
将一个值/计算带入一元上下文,而
lift
做同样的事情,除了将一元值包装在另一个值上?这是我从你和丹尼尔·瓦格纳那里得到的直觉。是的,基本上是t