同一个monad变压器的不同排序有什么区别?

Kei*_*man 25 monads haskell monad-transformers

我试图定义一个API来表示我的程序中的特定类型的过程.

newtype Procedure a = { runProcedure :: ? }
Run Code Online (Sandbox Code Playgroud)

有状态,包括ID到记录的映射:

type ID = Int
data Record = { ... }
type ProcedureState = Map ID Record
Run Code Online (Sandbox Code Playgroud)

有三个基本操作:

-- Declare the current procedure invalid and bail (similar to some definitions of fail for class Monad)
abort :: Procedure ()
-- Get a record from the shared state; abort if the record does not exist.
retrieve :: ID -> Procedure Record
-- Store (or overwrite) a record in the shared state.
store :: ID -> Record -> Procedure ()
Run Code Online (Sandbox Code Playgroud)

我对这些操作有几个目标:

  • 程序可以假设(与原始Map.lookup调用不同)关于哪些记录可用,如果他们的任何假设是错误的,则程序作为一个整体返回失败.
  • 可以使用<|>(来自Alternative类)将一系列过程链接在一起,以便回退到做出不同假设的过程.(与STM相似orElse)

鉴于这些目标,我相信我想要StateMaybemonad的一些组合.

-- Which to choose?
type Procedure a = StateT ProcedureState Maybe a
type Procedure a = MaybeT (State ProcedureState) a
Run Code Online (Sandbox Code Playgroud)

我无法弄清楚这两个排序MaybeState行为方式会有何不同.谁能解释两种排序之间的行为差​​异?

此外,如果你看到我原来思考的问题(也许我过度工程),请随意指出.

结论: 所有三个答案都有帮助,但有一个共同的想法,帮助我决定我想要的顺序.通过查看runMaybeT/ 的返回类型runStateT,很容易看出哪个组合具有我正在寻找的行为.(就我而言,我想要返回类型Maybe (ProcedureState, a)).

Joh*_*n L 23

编辑:我原本是倒退的.现在修复了.

monad变压器堆栈的排序之间的差异实际上只有当你剥离堆栈的层时才有意义.

type Procedure a = MaybeT (State ProcedureState) a
Run Code Online (Sandbox Code Playgroud)

在这种情况下,首先运行MaybeT,这会导致状态计算返回a Maybe a.

type Procedure a = StateT ProcedureState Maybe a
Run Code Online (Sandbox Code Playgroud)

这里StateT是外部monad,这意味着在以初始状态运行StateT之后,您将获得一个Maybe (a, ProcedureState).也就是说,计算可能已经成功,也可能没有.

所以你选择哪个取决于你想要如何处理部分计算.随着MaybeT在外面,你总是得到某种恢复的状态,无论计算的成功,这可能会或可能不会有用的.随着StateT在外面,你保证所有状态的交易是有效的.根据你描述的内容,我可能会StateT自己使用变体,但我希望其中任何一种都可以使用.

monad变换器排序的唯一规则是如果IO涉及(或另一个非变压器monad),它必须是堆栈的底部.通常情况ErrorT下,如果需要,人们将使用下一个最低级别.


Mat*_*ick 13

为了补充其他答案,我想在一般情况下描述如何解决这个问题.也就是说,给定两个变换器,它们的两种组合的语义是什么?

上周,当我开始在解析项目中使用monad变换器时,我遇到了很多麻烦.我的方法是创建一个转换类型的表,我不确定时会咨询.我是这样做的:

第1步:创建一个基本monad类型及其相应变换器类型的表:

transformer           type                  base type (+ parameter order)

---------------------------------------------------------------

MaybeT   m a        m (Maybe a)            b.    Maybe b

StateT s m a        s -> m (a, s)          t b.  t -> (b, t)

ListT    m a        m [a]                  b.    [] b

ErrorT e m a        m (Either e a)         f b.  Either f b

... etc. ...
Run Code Online (Sandbox Code Playgroud)

步骤2:将每个monad变换器应用于每个基础monad,替换为mtype参数:

inner         outer         combined type

Maybe         MaybeT        Maybe (Maybe a)
Maybe         StateT        s -> Maybe (a, s)      --  <==  this !!
... etc. ...

State         MaybeT        t -> (Maybe a, t)      --  <== and this !!
State         StateT        s -> t -> ((a, s), t)
... etc. ...
Run Code Online (Sandbox Code Playgroud)

(这一步有点痛苦,因为有一个二次数组合...但对我来说这是一个很好的练习,我只需要做一次.) 这里的关键是我写的组合类型解开 - 没有那些烦人的MaybeT,StateT等包装器.我可以更容易地看到并考虑没有样板的类型.

要回答您的原始问题,此图表显示:

  • MaybeT + State :: t -> (Maybe a, t) 状态计算,其中可能没有值,但总会有(可能已修改的)状态输出

  • StateT + Maybe :: s -> Maybe (a, s) 可以不存在状态和值的计算


Rei*_*ton 7

让我们假设你没有使用State/ StateT来存储程序的状态,而是IORefIOmonad 中使用了一个.

先验有两种方法可以让你想要mzero(或fail)在IOMaybemonad 的组合中表现:

  • 要么mzero消除整个计算,那么mzero <|> x = x; 要么
  • mzero导致当前计算不返回值,但IO保留了类型效果.

听起来你想要第一个,所以一个过程设置的状态被"展开"为<|>s 链中的下一个过程.

当然,这种语义是不可能实现的.我们不知道计算是否会mzero在我们运行之前调用,但这样做可能会产生任意IO影响launchTheMissiles,我们无法回滚.

现在,让我们尝试建立两个不同的单子转换堆出来的MaybeIO:

  • IOT Maybe - 哎呀,这不存在!
  • MaybeT IO

exists(MaybeT IO)给出了mzero可能的行为,而不存在IOT Maybe对应于其他行为.

幸运的是你正在使用State ProcedureState,其效果可以回滚,而不是IO; 你想要的monad变压器堆栈就是其中StateT ProcedureState Maybe之一.