【问题标题】:How to design a monadic stack?如何设计一元堆栈?
【发布时间】:2013-05-03 15:41:14
【问题描述】:

您如何设计和构建您的一元堆栈?我第一次需要构建一个单子堆栈(使用转换器)来解决现实世界的问题,但我并不确定堆栈转换器的顺序。如您所知,只要计算类型为* -> *,基本上任何东西都可以在转换器中扮演内部单子的角色,因此有几个问题:

  • 某些特定的转换器是否应该位于堆栈的顶部(例如 ReaderT?WriterT?)
  • 什么应该驱动设计?直觉?类型? (例如,根据您的 API 需求调整堆栈)
  • 每个堆栈是否彼此同构(在一定程度上),或者如果我错误地构建堆栈,我可能最终无法使用某些底层 monad 或出现一大堆臃肿的 @ 987654322@?我的直觉表明,如果转换器派生一些实例(例如 MonadReader、MonadIO 等,就像mtl 中的大多数转换器一样),那么我放置转换器的顺序并不重要。

我很想听听经验丰富的 Haskeller 提供的最佳实践或经验法则。

forever $ print "Thanks!"

一个。

【问题讨论】:

    标签: design-patterns haskell types monad-transformers


    【解决方案1】:

    这需要经验。要记住的一件事是,monad 转换器对它正在转换的 monad 一无所知,因此外部的 monad 受到内部行为的“约束”。所以

    StateT s (ListT m) a
    

    首先是由于内部单子而导致的不确定性计算。然后,将不确定性视为正常,添加状态 - 即不确定性的每个“分支”都有自己的状态。

    对比ListT (StateT s m) a,它主要是有状态的——即整个计算只有一个状态(模m),计算将在该状态下“单线程”运行,因为这就是@ 987654327@ 表示。非确定性将是最重要的——因此分支将能够观察到先前失败分支的状态变化。 (在这种特殊的组合中,这真的很奇怪,而且我从来不需要它)。

    这是 Dan Piponi 的 diagram,它提供了一些有用的直觉:

    我还发现扩展实现类型很有帮助,让我了解它是什么类型的计算。 ListT 很难扩展,但您可以将其视为“不确定”,StateT 易于扩展。所以对于上面的例子,我会看看

    StateT s (ListT m) a =~ s -> ListT m (a,s)
    

    即它接受一个传入状态,并返回 许多 个传出状态。这让您了解它是如何工作的。类似的方法是查看堆栈所需的run 函数的类型——这是否与您拥有的信息和您需要的信息相匹配?

    这里有一些经验法则。它们无法替代花时间通过扩展和查看来确定您真正需要的功能,但如果您只是在某种命令意义上寻找“添加功能”,那么这可能会有所帮助。

    ReaderTWriterTStateT 是最常见的转换器。首先,它们都相互通勤,因此您将它们放入的顺序无关紧要(如果您同时使用这三个,请考虑使用RWS)。此外,在实践中,我通常希望这些在外部,在内部使用“更丰富”的转换器,例如 ListTLogicTContT

    ErrorTMaybeT通常在上面三个的外面;让我们看看MaybeT如何与StateT交互:

    MaybeT (StateT s m) a =~ StateT s m (Maybe a) =~ s -> m (Maybe a, s)
    StateT s (MaybeT m) a =~ s -> MaybeT m (a,s) =~ s -> m (Maybe (a,s))
    

    MaybeT 在外部时,即使计算失败,也可以观察到状态变化。当MaybeT 在内部时,如果计算失败,您不会得到状态,因此您必须中止在失败计算中发生的任何状态更改。你想要其中哪一个取决于你想要做什么——然而,前者对应于命令式程序员的直觉。 (并不是说一定要努力)

    我希望这能让您了解如何考虑变压器堆栈,这样您就有更多工具来分析堆栈应该是什么样子。如果您将问题识别为 monadic 计算,那么让 monad 正确是最重要的决定之一,而且并不总是那么容易。花点时间探索各种可能性。

    【讨论】:

      【解决方案2】:

      这是一个相当广泛的问题。我只是给你一些基本的想法。

      首先,我建议尽可能保持基本 monad 的多态性。这将允许您在纯设置和 IO 设置中重用代码。这也将使您的代码更具可组合性。使用像MonadIO 这样的各种类也可以帮助你的代码保持更多的多态性,这通常是一件好事。

      需要注意的重要一点是,monad 转换器的顺序实际上控制着它们的语义。我最喜欢的例子是将ListT¹ 与EitherT 结合起来进行错误处理。如果外部有ListT,则整个 计算可能会失败并出现错误。如果你在外面有EitherT,那么每个分支都可以单独失败。因此,您实际上可以通过更改转换器的顺序来控制错误与非确定性交互的方式!

      如果您使用的 monad 转换器不依赖于顺序 - 例如我相信,将ReaderTWriterT 结合起来并不重要——然后就凭耳朵玩,并选择最适合您的应用程序的任何东西。这是一种随着经验而变得更容易的选择。

      ¹:来自Control.Monad.TransListT 有一些问题,所以假设它是ListT done right

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-06-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-09-16
        • 2022-08-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多