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

时间:2011-02-22 08:15:01

标签: haskell monads monad-transformers

我正在尝试定义一个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))。

5 个答案:

答案 0 :(得分:23)

编辑:我原本倒退了案件。现在修好了。

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用作下一个最低级别。

答案 1 :(得分: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. ...

第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 -> (Maybe a, t)有状态计算,其中可能没有值,但总会有(可能已修改)状态输出

  • StateT + Maybe :: s -> Maybe (a, s)计算,其中状态和值都可能不存在

答案 2 :(得分:7)

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

先验有两种方法可以让mzero(或fail)在IOMaybe monad的组合中表现出来:

  • mzero删除整个计算,以便mzero <|> x = x;或
  • mzero导致当前计算不返回值,但保留IO - 类型效果。

听起来你想要第一个,所以一个程序设置的状态被“展开”,用于<|> s链中的下一个程序。

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

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

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

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

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

答案 3 :(得分:4)

如果您尝试为两个版本编写“运行”功能,您将能够自己回答这个问题 - 我没有安装MTL +变换器,所以我自己无法做到。一个将返回(可能a,州)另一个可能(a,州)

编辑 - 我已经截断了我的回复,因为它添加了可能令人困惑的细节。约翰的回答击中了头部。

答案 4 :(得分:1)

摘要:不同的堆栈订单产生不同的业务逻辑

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

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

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

背景:StateTListT

  • StateTState monad在For a Few Monads More中得到了很好的解释。 StateT只是给你一点力量 - 使用它的基础m的monadic操作。如果您了解evalStateTputgetmodify就足够了,这些内容已在许多State monad教程中进行了解释。
  • ListTList,a.k.a,[],是一个monad(在A Fistful of Monads中解释)。 ListT m a(在包list-t中)为您提供类似于[a]的内容以及基础monad m的所有monadic操作。棘手的部分是执行ListT(类似于evalStateT):有很多执行方式。考虑使用evalStateTrunStateTexecState时您关注的不同结果,List monad的上下文有很多潜在的消费者,例如只是过了他们,即traverse_折叠,即fold等等。

实验:了解Monad变压器订单影响

我们将在StateT之上使用ListTIO构建一个简单的双层monad变换器堆栈,以实现演示的一些功能。

任务说明

汇总流中的数字

该流将被抽象为Integer的列表,因此我们的ListT进来。总结一下,我们需要在处理流中的每个项目时保持总和的状态,我们的StateT来了。

两个堆栈

我们有一个简单的状态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产生正确的结果,因为在StateT之后评估ListT。在评估StateT时,运行时系统已经评估了ListT的所有操作 - 使用流[6,7,8]向堆栈提供,并使用traverse_进行处理。此处评估这个词意味着ListT的效果消失,ListT现在对StateT透明。

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