展平 monad 栈

Flatten monad stack

所以我的第一个严肃的 haskell 项目中到处都是这种代码:

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

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

我也经常以 lift 链结束,我发现如果深层 monad 是 IO,可以用 liftIO 避免。不过,我不知道其他 monad 的通用方法。

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

是否可以在不提升整个块(这导致 let 和绑定变量的范围并限制在内部块)或者不必 lift . lift . ... lift 单独操作的情况下以某种方式操纵堆栈?

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

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

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

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

这通常是 monad 转换器的一个众所周知的问题。研究人员设计了各种处理方法,none 其中显然是 "best"。一些已知的解决方案包括:

  • 血淋淋的细节 mtl approach, which automatically lifts type classes of monads over its built-in monad transformers (and only its built-in monad transformers). This allows you to just write f :: (MonadState A m, MonadError () m) => m C if those are the only features of the monad that your function is using. Due to its extreme non-portability and a few other reasons, mtl is generally considered pseudo-deprecated. See this page and this question
  • 如果你有一个特定的 monad 堆栈,你会一遍又一遍地使用它,你可以将它包装在一个 newtype 中并编写各种 monad 类型的实例 类 支持手动。对于 FunctorApplicativeMonad 和堆栈中顶层转换器实现的任何其他类型 类,您可以使用 GeneralizedNewtypeDeriving 来获得编译器自动为你编写实例;对于其他类型 类,您必须为每个方法插入适当数量的 lift 调用。这种方法的优点是它更通用、更容易理解,同时在调用站点为您提供与 mtl 相同的灵活性。这种方法的一个大问题是它鼓励对所有操作使用单个 "mega-monad" 而不是仅指定所需的操作,因为将任何新的 monad 转换器添加到堆栈都需要编写一个全新的实例列表。
  • 在大多数情况下,您并不真的想要具有 "some arbitrary state of type A" 和 "some arbitrary exception-throwing capability" 的 monad。相反,你的 monad 栈提供的不同特性在你的程序心智模型中有一些语义。先前方法的一种变体是为基本 FunctorApplicativeMonad 之外的效果创建自定义类型 类,并为自定义类型 [=48] 编写实例=] 在你的 newtype 的 monad 上。与此处列出的其他方法相比,这有一个主要优势:您可以在堆栈中的不同位置包含多个副本的相同 monad 转换器。这是我迄今为止在自己的程序中使用最多的策略。
  • 一种完全不同的方法是效果系统。通常,效果系统必须内置到语言的类型系统中,但可以 encode an effect system in Haskell's type system. See the effect-monads 打包。