Gabriella439 / Haskell-MMorph-Library

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

Add MCoyoneda #33

Open juliapath opened 7 years ago

juliapath commented 7 years ago

Sometimes it can be difficult to implement an MFunctor instance for a given type due to the lack of a Monad constraint on the result type of hoist. Especially if you only have access to what is exported in the API for that type (e.g. 1, 2). However this can be worked around using a coyoneda construction like this (from my answer in the second link):

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

Would this be a useful addition to mmorph?

Gabriella439 commented 7 years ago

The only suggestion I'd make is to not add the MFunctor' class (mainly because I'd like to avoid type class proliferation) and instead replace lowerMCoyoneda with another function which I'm calling withCoyoneda:

{-# LANGUAGE RankNTypes                #-}
{-# LANGUAGE ExistentialQuantification #-}

import Control.Monad.Morph

-- Minor simplification to use just `ExistentialQuantification`
data MCoyoneda t n a =
    forall m . Monad m => MCoyoneda (forall b . m b -> n b) (t m a)

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

withCoyoneda
    :: (forall m . Monad m => (forall b . m b -> n b) -> t m a -> r)
    -> MCoyoneda t n a
    -> r
withCoyoneda k (MCoyoneda f tma) = k f tma

instance MFunctor (MCoyoneda t) where
  hoist f (MCoyoneda trans tma) = MCoyoneda (f . trans) tma

Then you can choose what hoist function to supply to withCoyoneda. For example, if you supply withCoyoneda with hoist, then you get this inferred type:

>>> :type withCoyoneda hoist
withCoyoneda hoist :: MFunctor t => MCoyoneda t n a -> t n a

... but if you supply withCoyoneda with hoist' then you get this inferred type:

>>> :type withCoyoneda hoist'
withCoyoneda hoist'
  :: (MFunctor' t, Monad n) => MCoyoneda t n a -> t n a

That then would let you keep MCoyoneda in mmorph but you could define MFunctor' separately and supply your hoist' function to withCoyoneda