4

所以我在我的第一个严肃的haskell项目中都有这种代码:

f :: (MonadTrans t) => ExceptT () (t (StateT A B)) C
f = do mapExceptT lift $ do
    lift $ do
        ...
        lift $ do
            ...
            r <- ...
            ...
            return r
    >>= \r -> ...

我如何尝试实现我的目标肯定可能有问题(可能有更简单的方法来做到这一点)但目前我有兴趣学习如何以更好的方式处理一堆 monad 转换器,如果有的话。这是我弄清楚如何进入r上下文B并将其提升到堆栈中更高的单子的唯一方法。举起整个块而不是最初的陈述是我自己能做到的。

我还经常得到的是我发现如果深层单子是lift可以避免的链。不过,我不知道其他单子的通用方式。liftIOIO

当他最终处理这样的堆栈时,是否有一种模式可以遵循,并且必须在某个级别提取一个值,在不同级别提取一个不同的值,结合这些并影响两个级别中的任何一个级别,或者可能还有另一个级别?

是否可以以某种方式操纵堆栈而无需提升整个块(这导致let和绑定变量的范围和限制到内部块),也不必lift . lift . ... lift单独操作?

4

2 回答 2

6

这通常是 monad 转换器的一个众所周知的问题。研究人员设计了各种处理方法,但没有一种方法显然是“最好的”。一些已知的解决方案包括:

  • mtl方法自动将类型类提升到其内置的 monad 转换器(并且只有其内置的 monad 转换器)之上。f :: (MonadState A m, MonadError () m) => m C如果这些是您的函数正在使用的 monad 的唯一功能,这允许您编写。由于其极端的不可移植性和其他一些原因,mtl通常被认为是伪弃用的。有关血腥细节,请参阅此页面此问题
  • 如果你有一个反复使用的特定newtypemonad 堆栈,你可以将它包装在 a 中并手动编写它支持的各种 monad 类型类的实例。对于Functor, Applicative, Monad, 以及由堆栈中的顶级转换器实现的任何其他类型类,您可以使用GeneralizedNewtypeDeriving让编译器自动为您编写实例;对于其他类型的类,您必须lift为每个方法插入适当数量的调用。这种方法的优点是它更通用、更易于理解,同时在呼叫站点为您提供与mtl. 这种方法的最大问题是它鼓励对所有操作使用单个“mega-monad”,而不是只指定所需的操作,因为将任何新的 monad 转换器添加到堆栈中需要编写一个全新的实例列表。
  • 在大多数情况下,您并不真正想要一个具有“某种任意类型状态A”和“某种任意异常抛出能力”的 monad。相反,您的 monad 堆栈提供的不同功能在您的程序的心理模型中具有一些语义含义。Functor先前方法的一种变体是为超出基本、、Applicative和的效果创建自定义类型类,并在您的 'd monadMonad上为自定义类型类编写实例。newtype与此处列出的其他方法相比,这具有一个主要优势:您可以拥有一个堆栈,其中在不同位置具有相同 monad 转换器的多个副本。这是迄今为止我在自己的程序中使用最多的策略。
  • 一个完全不同的方法是效果系统。通常,效果系统必须内置到语言的类型系统中,但可以在 Haskell 的类型系统中编码效果系统。请参阅effect-monads包。
于 2015-09-13T16:28:11.290 回答
4

通常的做法是使用mtl库而不是transformers直接使用。我不确定你背后的故事t是什么,但通常的mtl方法是在定义站点使用非常通用的类型签名,比如

foo :: (MonadError e m, MonadState s m) => m Int

然后在呼叫站点修复实际的变压器堆栈。一个常见的建议是将堆栈包装在一个新类型中,以避免在使用它们的地方混淆。

如果这不是你的风格(而且它并不适合所有人),你仍然可以使用这些mtl方法来执行操作,同时给出一个明确的转换器堆栈。这应该会大大减少手动提升。这种方法的优点是它可以让您更好地了解定义站点上效果的相互作用;缺点是更多的代码需要所有的信息。

于 2015-09-13T16:04:02.570 回答