【问题标题】:What is the difference between different orderings of the same monad transformers?同一个单子变压器的不同排序有什么区别?
【发布时间】:2011-07-01 19:54:35
【问题描述】:

我正在尝试定义一个 API 来表达我的程序中的特定类型的过程。

newtype Procedure a = { runProcedure :: ? }

有状态,由 ID 到记录的映射组成:

type ID = Int
data Record = { ... }
type ProcedureState = Map ID Record

有三个基本操作:

-- 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 ()

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

  • 程序可以做出假设 (不像原始的Map.lookup 电话)关于 哪些记录可用,如果 他们的任何假设都是错误的, 整个过程返回 失败。
  • 一系列程序可以 使用<|> 链接在一起(来自 类替代)以下降 返回程序 不同的假设。 (相似 STM 的orElse)

鉴于这些目标,我相信我想要一些 StateMaybe monad 的组合。

-- Which to choose?
type Procedure a = StateT ProcedureState Maybe a
type Procedure a = MaybeT (State ProcedureState) a

我无法弄清楚MaybeState 的两个排序将如何表现不同。 谁能解释这两种排序之间的行为差​​异?

另外,如果您发现我最初的想法有问题(也许我过度设计),请随时指出。

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

【问题讨论】:

    标签: haskell monads monad-transformers


    【解决方案1】:

    编辑:我最初是把箱子倒过来的。现已修复。

    monad 转换器堆栈的顺序之间的差异只有在您剥离堆栈层时才真正重要。

    type Procedure a = MaybeT (State ProcedureState) a
    

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

    type Procedure a = StateT ProcedureState Maybe a
    

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

    因此,您选择哪一种取决于您希望如何处理部分计算。在外部使用MaybeT,无论计算是否成功,您总是会得到某种返回状态,这可能有用也可能没有用。在外部使用StateT,您保证所有有状态的事务都是有效的。根据您的描述,我自己可能会使用StateT 变体,但我希望任何一个都可以工作。

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

    【讨论】:

    • 如果你通过 lambdabot 的 @unmtl 运行两个 monad 转换器堆栈,那么第一个变成 ProcedureState -> (Maybe a, ProcedureState) 和第二个 ProcedureState -> Maybe (a, ProcedureState),所以我认为你已经让它们倒退了。 run...T 函数的类型令人困惑!
    • @Reid Barton,非常感谢。你完全正确,我让他们倒退了。编辑更正。
    【解决方案2】:

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

    上周我开始在解析项目中使用 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. ...
    

    第 2 步:将每个 monad 转换器应用到每个基本 monad,替换 m 类型参数:

    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. ...
    

    (这一步有点痛苦,因为有二次元的组合……但对我来说这是一个很好的练习,我只需要做一次。)我在这里是我编写了组合类型 unwrapped —— 没有那些烦人的 MaybeT、StateT 等包装器。对我来说,查看和思考没有样板的类型要容易得多。

    为了回答您最初的问题,此图表显示:

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

    • StateT + Maybe :: s -&gt; Maybe (a, s) 状态和值都可能不存在的计算

    【讨论】:

    • 优秀的答案。
    【解决方案3】:

    让我们假设您在IO monad 中使用IORef,而不是使用State/StateT 来存储过程的状态。

    您可能希望mzero(或fail)在IOMaybe monad 的组合中以两种方式运行:

    • mzero 会清除整个计算,因此mzero &lt;|&gt; x = x;或
    • mzero 导致当前计算不返回值,但保留IO-type 效果。

    听起来您想要第一个,以便一个过程设置的状态为&lt;|&gt;s 链中的下一个过程“展开”。

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

    现在,让我们尝试用MaybeIO 构建两个不同的monad 转换器堆栈:

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

    存在的(MaybeT IO)给出了可能的mzero行为,不存在的IOT Maybe对应于另一种行为。

    幸好你使用的是State ProcedureState,它的效果可以回滚,而不是IO;您想要的 monad 转换器堆栈是 StateT ProcedureState Maybe 之一。

    【讨论】:

      【解决方案4】:

      如果您尝试为这两个版本编写“运行”函数,您将能够自己回答这个问题 - 我没有安装 MTL + 转换器,所以我无法自己完成。一个会返回 (Maybe a,state) 另一个 Maybe (a,state)

      编辑 - 我已经截断了我的回复,因为它添加了可能令人困惑的细节。约翰的回答一针见血。

      【讨论】:

        【解决方案5】:

        总结:不同的堆栈顺序产生不同的业务逻辑

        也就是说,堆栈的不同monad转换器顺序不仅会影响评估顺序,还会影响程序的功能。

        在演示订单的影响时,人们通常使用最简单的转换器,例如ReaderTWriterTStateTMaybeTExceptT。它们的不同顺序不会给出显着不同的业务逻辑,因此很难清楚地理解其影响。此外,它们的一些子集是可交换的,即没有功能差异。

        出于演示目的,我建议使用StateTListT,它们揭示了monad 堆栈上transformer order 之间的巨大差异。

        背景:StateTListT

        • StateT: State monad 在 For a Few Monads More 中有很好的解释。 StateT 只是给你更多的力量——使用它底层的m 的一元操作。如果您知道evalStateTputgetmodify 就足够了,这些在许多State monad 教程中都有说明。
        • ListTList,又名[],是一个单子(在A Fistful of Monads 中解释)。 ListT m a(在包 list-t 中)为您提供类似于 [a] 的内容以及底层 monad m 的所有单子操作。棘手的部分是ListT 的执行(类似于evalStateT):有很多执行方式。想想你在使用 evalStateTrunStateTexecState 时关心的不同结果,List monad 的上下文有很多潜在的消费者,例如只是检查他们,即,@ 987654354@,折叠它们,即fold,等等。

        实验:了解 Monad Transformer 顺序影响

        我们将在IO 之上使用StateTListT 构建一个简单的两层monad 转换器堆栈,以实现一些功能进行演示。

        任务说明

        汇总流中的数字

        流将被抽象为Integers 的列表,因此我们的ListT 进来了。总结它们,我们需要在处理流中的每个项目时保持总和的状态,其中我们的@ 987654361@来了。

        两堆

        我们有一个简单的状态 Int 来保持总和

        • ListT (StateT Int IO) a
        • StateT Int (ListT IO) a

        完整程序

        #!/usr/bin/env stack
        -- stack script --resolver lts-11.14 --package list-t --package transformers
        
        import ListT (ListT, traverse_, fromFoldable)
        import Control.Monad.Trans.Class (lift)
        import Control.Monad.IO.Class (liftIO)
        import Control.Monad.Trans.State (StateT, evalStateT, get, modify)
        
        main :: IO()
        main =  putStrLn "#### Task: summing up numbers in a stream"
             >> putStrLn "####       stateful (StateT) stream (ListT) processing"
             >> putStrLn "#### StateT at the base: expected result"
             >> ltst
             >> putStrLn "#### ListT at the base: broken states"
             >> stlt
        
        
        
        -- (ListT (StateT IO)) stack
        ltst :: IO ()
        ltst = evalStateT (traverse_ (\_ -> return ()) ltstOps) 10
        
        ltstOps :: ListT (StateT Int IO) ()
        ltstOps = genLTST >>= processLTST >>= printLTST
        
        genLTST :: ListT (StateT Int IO) Int
        genLTST = fromFoldable [6,7,8]
        
        processLTST :: Int -> ListT (StateT Int IO) Int
        processLTST x = do
            liftIO $ putStrLn "process iteration LTST"
            lift $ modify (+x)
            lift get
        
        printLTST :: Int -> ListT (StateT Int IO) ()
        printLTST = liftIO . print
        
        
        
        -- (StateT (ListT IO)) stack
        stlt :: IO ()
        stlt = traverse_ (\_ -> return ())
             $ evalStateT (genSTLT >>= processSTLT >>= printSTLT) 10
        
        genSTLT :: StateT Int (ListT IO) Int
        genSTLT = lift $ fromFoldable [6,7,8]
        
        processSTLT :: Int -> StateT Int (ListT IO) Int
        processSTLT x = do
            liftIO $ putStrLn "process iteration STLT"
            modify (+x)
            get
        
        printSTLT :: Int -> StateT Int (ListT IO) ()
        printSTLT = liftIO . print
        

        结果及说明

        $ ./order.hs   
        #### Task: summing up numbers in a stream
        ####       stateful (StateT) stream (ListT) processing
        #### StateT at the base: expected result
        process iteration LTST
        16
        process iteration LTST
        23
        process iteration LTST
        31
        #### ListT at the base: broken states
        process iteration STLT
        16
        process iteration STLT
        17
        process iteration STLT
        18
        

        第一个堆栈ListT (StateT Int IO) a 产生正确的结果,因为StateTListT 之后进行评估。在评估StateT 时,运行时系统已经评估了ListT 的所有操作——向堆栈提供流[6,7,8],并通过traverse_ 遍历它们。 已评估这个词在这里表示ListT 的影响已经消失,ListT 现在对StateT 是透明的。

        第二个堆栈StateT Int (ListT IO) a 没有正确的结果,因为StateT 的生命周期太短。在ListT 评估(又名traverse_)的每次迭代中,都会创建、评估和消失状态。此堆栈结构中的StateT 无法实现在列表/流项操作之间保持状态的目的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-10-22
          • 2010-10-02
          • 1970-01-01
          • 2017-05-14
          • 2013-06-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多