状态单子:从一种状态类型转换到另一种状态类型
Posted
技术标签:
【中文标题】状态单子:从一种状态类型转换到另一种状态类型【英文标题】:State monads: Transitioning from one state type to another 【发布时间】:2016-05-06 00:00:46 【问题描述】:假设我们有一堆单子,其中状态单子转换器作为最外层的转换器,如下所示:
-- | SEWT: Composition of State . Except . Writer monad transformers in that
-- order where Writer is the innermost transformer.
-- the form of the computation is: s -> (Either e (a, s), w)
newtype SEWT s e w m a = SEWT
_runSEWT :: StateT s (ExceptT e (WriterT w m)) a
deriving (Functor, Applicative, Monad,
MonadState s, MonadError e, MonadWriter w)
-- | 'runSEWT': runs a 'SEWT' computation given an initial state.
runSEWT :: SEWT s e w m a -> s -> m (Either e (a, s), w)
runSEWT ev e = runWriterT $ runExceptT $ runStateT (_runSEWT ev) e
然后我们想以某种形式:SEWT s e w m a -> s -> SEWT t e w m a
。
这当然不可能使用(>>=)
或do
块,因为以s
作为状态的状态单子与带有t
的单子不同。
然后我可以想出这样的东西:
-- | 'sewtTransition': transitions between one 'SEWT' computation with state s,
-- to another with state s. The current state and result of the given
-- computation is given to a mapping function that must produce the next
-- computation. The initial state must also be passed as the last parameter.
transitionState :: (Monad m, Monoid w) => ((a, s) -> SEWT t e w m a)
-> m (SEWT s e w m a) -> s -> m (SEWT t e w m a)
transitionState _trans _comp _init = do
(res, logs) <- _comp >>= flip runSEWT _init
return $ do tell logs
case res of Left fail -> throwError fail
Right succ -> _trans succ
-- 'withState': behaves like 'transitionState' but ignores the state of
-- the first computation.
withState :: (Monad m, Monoid w)
=> m (SEWT s e w m a) -> s -> m (SEWT t e w m a)
withState = transitionState $ return . fst
但是有没有更优雅和通用的方式来从一种状态类型转移到另一种状态类型?
我对第二次计算不依赖于第一次计算的最终状态(仅结果)的解决方案以及它所在的解决方案都感兴趣。
Edit1:改进的过渡功能:
transSEWT :: Functor m => (((a, y), x) -> (a, y)) -> SEWT x e w m a -> x -> SEWT y e w m a
transSEWT f x_c x_i = SEWT $ StateT $ \y_i -> ExceptT . WriterT $
first ((\(a, x_f) -> f ((a, y_i), x_f)) <$>) <$> runSEWT x_c x_i
changeSEWT :: Functor m => SEWT x e w m a -> x -> SEWT y e w m a
changeSEWT = transSEWT fst
transS :: Monad m => (((a, y), x) -> (a, y)) -> StateT x m a -> x -> StateT y m a
transS f x_c x_i = StateT $ \y_i -> do (a, x_f) <- runStateT x_c x_i
return $ f ((a, y_i), x_f)
changeS :: Monad m => StateT x m a -> x -> StateT y m a
changeS = transS fst
【问题讨论】:
与其他问题一起链接:***.com/questions/28690448/what-is-indexed-monad 【参考方案1】:你的想法可以用索引状态单子来实现。
newtype IState i o a = IState runIState :: i -> (o, a)
IState i o a
类型的值是一个有状态计算,它返回一个a
类型的值,在此过程中将隐式状态的类型从i
转换为o
。将此与常规的 State
monad 进行对比,后者不允许您更改其状态的类型:
type State s = IState s s
排序索引状态单子应确保输入和输出对齐。一个计算的输出类型是下一个计算的输入。输入 Atkey 的 parameterised monad(现在通常称为 indexed monad),这是一类描述通过有向图的路径的类 monad。
class IMonad m where
ireturn :: a -> m i i a
(>>>=) :: m i j a -> (a -> m j k b) -> m i k b
(>>>) :: IMonad m => m i j a -> m j k b -> m i k b
mx >>> my = mx >>>= const my
绑定一个索引单子就像玩多米诺骨牌:如果你有办法从i
到j
和从j
到k
的方法,>>>=
将把你的多米诺骨牌粘在一起从i
到k
的更大计算。 McBride 在 Kleisli Arrows of Outrageous Fortune 中描述了这个索引 monad 的一个更强大的版本,但这对于我们的目的来说已经足够了。
如上所述,多米诺骨牌式排序正是索引状态 monad 所需要的,它需要对齐输入和输出。
instance IMonad IState where
ireturn x = IState $ \s -> (s, x)
IState f >>>= g = IState $ \i -> let (o, x) = f i
in runIState (g x) o
从索引状态单子中检索值不会更改状态的类型。
get :: IState s s s
get = IState $ \s -> (s, s)
将值放入索引状态 monad 会丢弃旧状态。这意味着输入状态的类型可以是任何你喜欢的。
put :: s -> IState i s ()
put x = IState $ \_ -> (x, ())
请注意,与IState
一起使用的所有代码都与State
完全相同!只是类型变得更聪明了。
这是一个简单的IState
计算,它需要Int
类型的状态,将状态更改为String
,并返回一个布尔值答案。所有这些都是静态检查的。
myStateComputation :: IState Int String Bool
myStateComputation =
-- poor man's do notation. You could use RebindableSyntax
get >>>= \s ->
put (show s) >>>
ireturn (s > 5)
main = print $ runIState myStateComputation 3
-- ("3", False)
【讨论】:
这真是阅读的乐趣。我想我们在 Kleisli 类别中有>>=>> :: IMonad m => (a -> m i j b) -> (b -> m j k c) -> a -> m i k c
。如果您可以扩展几点:a)为此使用 RebindableSyntax 是否“常见”? b) 有Control.Monad.Indexed
,但是是否有一些 hackage 包可以将它与上面的 monad 变压器堆栈一起使用,还是我需要自己推出?
只是在这里吐口水,但我希望索引 monad transformers 在 McBride 风格的索引方案中可能会比在 Atkey 风格的索引方案中更顺畅。 McBride 的想法的好处在于,一旦你了解了基础知识,所有正常的 monad 内容都会直接翻译:transformers 将存在于((k -> *) -> (k -> *)) -> ((k -> *) -> (k -> *))
中,而ilift :: m a ~> t m a
则为type f ~> g = forall i. f i -> g i
。请阅读 Kleisli Arrows of Outrageous Fortune 了解完整故事。
关于RebindableSyntax
- 这种事情正是扩展的设计目的。使用它是否真的是一个好主意实际上是一个工程决策 - 我会在打开它之前考虑诸如谁将阅读代码?之类的事情。
我浏览了 Outrageous Fortune 论文,但我发现 Atkeys 版本更容易掌握,我想我得再看一下 =) 设法用 Edward Ekmetts 代码重新创建了我的 SEWT
转换器作为基础:gist.github.com/Centril/a87d72dc753e0cf71133568de53eb935 我的具体用例是编译器的前端和后端 (LLVM) 之间的转换。
是的,McBride 的想法肯定更难掌握,但一旦你“明白”了它就会变得更丰富、更简单。正如我认为您从编写要点中学到的那样,使用像IMonad
这样的非标准位的问题在于,您最终会编写很多无法与像transformers
这样的标准位很好集成的管道。对于“实际工作”,我可能会坚持使用常规 monad 并寻找重新设计代码的方法,例如删除 State
并在代码中使用可见的 state 变量。以上是关于状态单子:从一种状态类型转换到另一种状态类型的主要内容,如果未能解决你的问题,请参考以下文章
如何为 UIControl/UIButton 动画从一种状态到另一种状态的过渡? [复制]