5

Let's say we have a stack of monads with a state monad transformer as the outer most transformer like this one:

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

We then want to do, in some form: SEWT s e w m a -> s -> SEWT t e w m a. This is of course not possible using (>>=) or a do block since a state monad with s as state is not the same monad as one with t.

I can then conjure up something like this:

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

But is there perhaps a more elegant and general way to move from one state type to another?

I'm interested both in solutions where the second computation is not dependent on the final state (only the result) of the first computation, and one where it is.

Edit1: Improved transition functions:

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
Centril
  • 2,549
  • 1
  • 22
  • 30
  • linking together with other question: http://stackoverflow.com/questions/28690448/what-is-indexed-monad – Centril May 07 '16 at 19:05

1 Answers1

9

Your idea can be implemented with the indexed state monad.

newtype IState i o a = IState { runIState :: i -> (o, a) }

A value of type IState i o a is a stateful computation which returns a value of type a, transforming the type of the implicit state from i to o in the process. Contrast this with the regular State monad, which doesn't allow you to change the type of its state:

type State s = IState s s

Sequencing indexed state monads should ensure that the inputs and outputs line up. The output type of one computation is the input of the next. Enter Atkey's parameterised monad (now more commonly known as the indexed monad), the class of monad-like things describing a path through a directed graph.

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

Binding an indexed monad is like playing dominoes: if you have a way to get from i to j and a way to get from j to k, >>>= will glue your dominoes together into a bigger computation which goes from i to k. McBride describes a more powerful version of this indexed monad in Kleisli Arrows of Outrageous Fortune, but this one is quite enough for our purposes.

As I described above, domino-like sequencing is just what's needed for the indexed state monad, which requires alignment of inputs and outputs.

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

Retrieving a value from an indexed state monad doesn't change the type of the state.

get :: IState s s s
get = IState $ \s -> (s, s)

Putting a value into the indexed state monad discards the old state. This means the type of the input state can be anything you like.

put :: s -> IState i s ()
put x = IState $ \_ -> (x, ())

Note that all the code to work with IState is exactly the same as State! It's just the types which have got smarter.

Here's a simple IState computation which expects a state of type Int, changes the state to a String, and returns a Boolean answer. All of this is statically checked.

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)
Benjamin Hodgson
  • 42,952
  • 15
  • 108
  • 157
  • This was a true pleasure to read. I guess we then have `>>=>> :: IMonad m => (a -> m i j b) -> (b -> m j k c) -> a -> m i k c` in the Kleisli category. If you could expand on a few points: a) Is it "common" to use RebindableSyntax for this? b) There's `Control.Monad.Indexed`, but is there some hackage package for using this with a monad transformer stack like the one above, or do I need to roll my own? – Centril May 07 '16 at 00:19
  • Just spitballing here, but I expect indexed monad _transformers_ would probably go smoother in a McBride-style indexing scheme than in an Atkey-style one. The nice thing about McBride's idea is that once you've understood the basics, all of the normal monad stuff translates directly: transformers would inhabit the kind `((k -> *) -> (k -> *)) -> ((k -> *) -> (k -> *))` and you'd have `ilift :: m a ~> t m a` where `type f ~> g = forall i. f i -> g i`. Have a read of _Kleisli Arrows of Outrageous Fortune_ for the full story. – Benjamin Hodgson May 07 '16 at 10:22
  • Regarding `RebindableSyntax` - this sort of thing is exactly what the extension was designed for. Whether or not it's actually a good idea to use it is really an engineering decision - I'd consider things like _who's going to read the code?_ before turning it on. – Benjamin Hodgson May 07 '16 at 10:25
  • I skimmed through the Outrageous Fortune paper, but I found Atkeys version easier to grasp, guess I'll have to look at it some more =) Managed to recreate my `SEWT` transformer with Edward Ekmetts code as a base: https://gist.github.com/Centril/a87d72dc753e0cf71133568de53eb935 My concrete use case is the transition between the frontend and backend (LLVM) of a compiler. – Centril May 07 '16 at 10:46
  • Yeah, McBride's idea is definitely harder to grasp but it's richer and simpler once you've "got it". As I think you've learned from writing your gist, the problem with using non-standard bits like `IMonad` is that you end up writing a lot of plumbing that doesn't integrate well with standard bits like `transformers`. For "real work" I'd probably stick to regular monads and look for a way to redesign my code, such as dropping `State` and living with the visible presence of the state variable in the code. – Benjamin Hodgson May 07 '16 at 10:57
  • One thing I noticed is that you can't get `throwError :: e -> Test j k a`... instead you get `throwError :: e -> Test j j a` which is problematic since you can't throw in transitioning contexts... If you have time, could please exemplify the indexed state monad in McBridge-encoding, for example: converting `myStateComputation`? Even after reading https://www.eyrie.org/~zednenem/2012/07/29/paramonads I don't understand it entierly. If you could perhaps add this to your answer for future reference that'd be nice too. – Centril May 07 '16 at 14:29
  • I think `throwError :: e -> Test j j a` is the correct type. Throwing an error doesn't change the type of the state variable. `throwError` can be pre- or post-composed with the computation of your choice without changing the index. Like a type-level `id` – Benjamin Hodgson May 07 '16 at 15:37
  • Indeed, it seems you are correct. Tested it now with my Test monad and it works as expected. – Centril May 07 '16 at 16:18