Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does MFunctor's 'hoist' not have 'Monad n' constraint?

Tags:

haskell

I have a coroutine transformer

data Step y m a = Done a | Yield y (CoT y m a)

data CoT y m a = CoT (m (Step y m a))

with Monad instance

unCoT :: CoT y m a -> m (Step y m a)
unCoT (CoT m) = m

instance Monad m => Monad (CoT y m) where
    return  = CoT . return . Done
    CoT x >>= f = CoT $ do
      x' <- x
      case x' of
        Done a -> unCoT (f a)
        Yield y x' -> return (Yield y (x' >>= f))

If I define an MFunctor class with Monad m and Monad n constraints I can define hoist

class MFunctor t where
  hoist :: (Monad n, Monad m) => (forall a. m a -> n a) -> t m b -> t n b

instance MFunctor (CoT y) where
  hoist f (CoT m) = CoT $ do
    step <- f m
    return (case step of Done x     -> Done x
                         Yield y m' -> Yield y (hoist f m'))

But mmorph's hoist only has a Monad m constraint. Can I define my hoist without it, or is this a lack of generality of MFunctor?

EDIT: I worked out it is possible! But my question still stands: are we sure there's no lack of generality here?

instance MFunctor (CoT y) where
  hoist f (CoT m) = CoT $ f $ do
    step <- m
    return (case step of Done x     -> Done x
                         Yield y m' -> Yield y (hoist f m'))
like image 865
Tom Ellis Avatar asked Jan 27 '14 21:01

Tom Ellis


2 Answers

mmorph was developed in the context of the pipes-3.* series (it used to be an internal pipes module), which had functions like this:

raise
    :: (Monad m, MFunctor t1, MonadTrans t2)
    => t1 m r -> t1 (t2 m) r
raise = hoist lift

If you add the Monad n constraint to hoist then you have to add a Monad (t2 m) constraint to raise. I generally try to minimize constraints in my libraries and I couldn't find any MFunctor instances that needed the Monad n constraint, so I removed it.

Side note: CoT y m a is the same thing as Producer y m a from pipes, which already has an MFunctor instance.

like image 185
Gabriella Gonzalez Avatar answered Oct 08 '22 12:10

Gabriella Gonzalez


You can use any type t for which you can define hoist' :: (Monad m, Monad n) => (forall t. m t -> n t) -> t m a -> t n a as a MFunctor. But you will only be able to use the resulting t n a if you have a Monad instance on n. We do this by deferring the application of the natural transformation. Or a fancy way of saying this would be applying the coyoneda lemma.

{-# LANGUAGE RankNTypes, GADTs #-}

import Control.Monad.Morph

-- Slightly weaker than MFunctor due to the monad constraint on n.
class MFunctor' t where
  hoist' :: (Monad m, Monad n) => (forall b. m b -> n b) -> t m a -> t n a

data MCoyoneda t n a where
  MCoyoneda :: Monad m => (forall b. m b -> n b) -> t m a -> MCoyoneda t n a

liftMCoyoneda :: Monad m => t m a -> MCoyoneda t m a
liftMCoyoneda = MCoyoneda id

lowerMCoyoneda' :: (MFunctor' t, Monad n) => MCoyoneda t n a -> t n a
lowerMCoyoneda' (MCoyoneda f tma) = hoist' f tma

-- The result is actually slightly stronger than 'MFunctor', as we do not need
-- a monad for 'm' either.
hoistMCoyoneda :: (forall b. m b -> n b) -> MCoyoneda t m a -> MCoyoneda t n a
hoistMCoyoneda f (MCoyoneda trans tma) = MCoyoneda (f . trans) tma

instance MFunctor (MCoyoneda t) where
  hoist = hoistMCoyoneda

So I don't think we loose generality, because if you can not implement an instance for MFunctor then nothing is lost by the Monad constraint on lowerMCoyoneda'.

I discovered this running into a similar problem with RVarT.

like image 1
Julia Path Avatar answered Oct 08 '22 11:10

Julia Path