27

我正在尝试定义一个 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调用不同),如果它们的任何假设是错误的,则整个过程会返回失败。
  • 可以使用(来自类 Alternative)将一系列过程链接在一起<|>,以便回退到做出不同假设的过程。(类似于 STM 的orElse

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

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

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

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

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

4

5 回答 5

25

编辑:我最初把箱子倒过来了。现在修好了。

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通常,如果需要,人们会将其用作下一个最低级别。

于 2011-02-22T09:05:23.223 回答
15

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

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

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

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

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

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

于 2012-12-05T13:34:43.497 回答
8

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

先验地,您可能希望mzero(或fail)以两种方式在 theIOMaybemonad 的组合中表现:

  • 要么mzero消灭整个计算,所以mzero <|> x = x; 或者
  • mzero导致当前计算不返回值,但IO保留 -type 效果。

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

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

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

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

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

幸运的是,您使用的是State ProcedureState,其效果可以回滚,而不是IO; 你想要的单子变压器堆栈就是StateT ProcedureState Maybe一个。

于 2011-02-22T15:10:16.320 回答
4

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

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

于 2011-02-22T09:05:57.180 回答
1

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

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

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

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

背景:StateTListT

  • StateT: Statemonad 在For a Few Monads More中有很好的解释。StateT只是给你更多的权力——使用它底层的一元操作m。如果您知道evalStateT, put,getmodify, 就足够了,这些在许多Statemonad 教程中都有解释。
  • ListT: List, aka, [], 是一个 monad(在A Fistful of Monads中有解释)。ListT m a(在 package 中list-t)为您提供类似于[a]plus 底层 monad 的所有 monadic 操作的东西m。棘手的部分是执行ListT(类似于evalStateT):有很多执行方式。evalStateT想想你在使用,runStateT和时关心的不同结果,monadexecState的上下文List有很多潜在的消费者,例如只是遍历它们,即traverse_折叠它们,即,fold等等。

实验:了解 Monad 变压器阶数影响

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

任务描述

汇总流中的数字

流将被抽象为Integers 的列表,所以我们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评估的每次迭代中,也就是,traverse_状态被创建、评估和消失。在StateT这个堆栈结构中并没有达到其在列表/流项目操作之间保持状态的目的。

于 2018-06-18T20:18:10.483 回答