Gabriella439 / Haskell-MMorph-Library

Monad morphisms
BSD 3-Clause "New" or "Revised" License
47 stars 26 forks source link

Having trouble deriving MMonad for monad stacks #34

Open jacobstanley opened 7 years ago

jacobstanley commented 7 years ago

Given some random transformer stack, where all of the transformers implement MMonad:

newtype FooT m a = FooT { unFoo :: ExceptT () (StateT Int (BarT m)) a }

I'm finding it difficult to create instances of MMonad for Foo, without just running everything and manually implementing embed for the entire stack.

Am I missing something, or is this because monads don't compose? Is there an equivalent of monad transformers for the category of monads? Maybe that would make creating these instances easier.

Any thoughts appreciated, hopefully I'm just missing something obvious.

RyanGlScott commented 7 years ago

When you say "deriving MMonad", are you literally trying this?

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

newtype FooT m a = FooT { unFoo :: ExceptT () (StateT Int (BarT m)) a }
  deriving MMonad

If so, that doesn't work by design, as you can't eta-reduce m a from ExceptT () (StateT Int (BarT m)) a. You could do this, however, if you slightly refactor FooT:

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeOperators #-}

import Control.Monad.Trans.Compose

newtype FooT m a = FooT { unFoo :: (ExceptT () `ComposeT` StateT Int `ComposeT` BarT) m a }
  deriving MMonad

Now m a appears unencumbered on the RHS, so it's possible to eta-reduce it for the purposes of deriving MMonad!

...except that mmorph currently doesn't provide MMonad instances for ExceptT and StateT. That should probably be fixed...

jacobstanley commented 7 years ago

When you say "deriving MMonad", are you literally trying this?

No, I mean deriving by hand.

You could do this, however, if you slightly refactor FooT

Interesting! Are there any downsides to that?

RyanGlScott commented 7 years ago

Are there any downsides to that?

I suppose there's a downside in the sense that if you want to define other common mtl-like instances for FooT, you'll also need to make sure that ComposeT has a corresponding instance as well. Defining instances for ComposeT tends to be pretty straightforward in my experience, although I've never found a way to implement MonadTransControl or MonadBaseControl for ComposeT... maybe someone smarter than me can figure it out :)

jacobstanley commented 7 years ago

Hmm damn, GHC can't typecheck the derived code, I'll see if I can get an example together when I get time.

RyanGlScott commented 7 years ago

Actually, I fear that I might have lead you astray with bad advice. You wouldn't just need MMonad instances for ExceptT and StateT, you'd also need one for ComposeT as well. And as far as I can tell, creating a MMonad instance for ComposeT is impossible in current GHC, as you'd need a constraint like forall m . (Monad m) => Monad (g m), which would require something like implication constraints. Oh well.

But this is a red herring, since you said you weren't using GeneralizedNewtypeDeriving in the first place. So, let me try to get back on track... what were you trying to do? :)

jacobstanley commented 7 years ago

I was just trying to implement MMonad manually for a rather large stack, and I couldn't figure out how to use the fact that each transformer in the stack is already MMonad.

I just ended up unwrapping/running everything, doing the bind, and then putting it all back together, which was very unsatisfying and error prone. You end up with the bind for every transformer, all smashed together in the same function.

Is there a way to implement MMonad using the embed functions which the transformers already have, or will I run in to the same issue as ComposeT?

Maybe try with ExceptT () (WriterT [()] m) as an example?

Gabriella439 commented 7 years ago

@jystic: Here's the general trick:

import Control.Monad.Trans.Identity
import Control.Monad.Morph

newtype Example m a =
    Example { runExample :: IdentityT (IdentityT (IdentityT m)) a }

instance MFunctor Example where
    hoist f (Example m) = Example (hoist (hoist (hoist f)) m)

instance MonadTrans Example where
    lift m = Example (lift (lift (lift m)))

instance MMonad Example where
    embed f m =
        Example (embed (embed (embed (runExample . f))) (runExample m))
jacobstanley commented 7 years ago

@Gabriel439, thanks for the help.

I can't seem to get that to type check if I change one of the IdentityT transformers to a different transformer, say ExceptT.

RyanGlScott commented 7 years ago

I'm convinced @Gabriel439's example only works precisely because it's using the same monad transformer repeatedly. As @jystic discovered, if you try swapping out one of the IdentityTs out with a different transformer, then the iterated embed trick no longer works.

I'm convinced that doing this in a compositional way is tantamount to writing a MMonad instance for ComposeT which, as noted in https://github.com/Gabriel439/Haskell-MMorph-Library/issues/34#issuecomment-288390248, is currently impossible. So I don't see any other way to do this other than the all-at-once approach from https://github.com/Gabriel439/Haskell-MMorph-Library/issues/34#issue-215918776.

Gabriella439 commented 7 years ago

Oops, you're right. In fact, it shouldn't be possible in general because MMonads don't compose for the same reason that Monads don't compose: you need a distributive law. The reason my example worked is because IdentityTs (trivially) distribute

jacobstanley commented 7 years ago

So then this comes back to my original question:

Am I missing something, or is this because monads don't compose? Is there an equivalent of monad transformers for the category of monads?

Is something like MMonad transformers possible? What would that even look like?

Gabriella439 commented 7 years ago

My intuition is the higher-kinded analogy would be something like this:

{-# LANGUAGE KindSignatures #-}

import Control.Monad.Trans.Except
import Control.Monad.Morph

newtype ExceptTT e t (m :: * -> *) a = ExceptTT { runExceptTT :: t (ExceptT e m) a }

instance MFunctor t => MFunctor (ExceptTT e t) where
    hoist f (ExceptTT m) = ExceptTT (hoist (hoist f) m)

instance (MFunctor t, MonadTrans t) => MonadTrans (ExceptTT e t) where
    lift m = ExceptTT (hoist lift (lift m))

instance MMonad t => MMonad (ExceptTT e t) where
    embed f = ???

... but I think I need more constraints on t to implement the final MMonad instance. I got stuck trying to implement it

edsko commented 5 years ago

...except that mmorph currently doesn't provide MMonad instances for ExceptT and StateT. That should probably be fixed...

Can StateT be given an MMonad instance? I thought it was not possible.

Gabriella439 commented 5 years ago

@edsko: You're right: StateT cannot be given an MMonad instance