[Question] Associated type-families instead of `MultiParamTypeclasses` and `FunctionalDependencies` for `MonadLog`
Closed this issue · 4 comments
Is there a reason MultiParamTypeclasses
and FunctionalDependencies
are used in the MonadLog
typeclass instead of associated type families?
e.g.
class Monad m => MonadLog message m | m -> message where
logMessageFree :: (forall n. Monoid n => (message -> n) -> n) -> m ()
default logMessageFree :: (m ~ t n, MonadTrans t, MonadLog message n) => (forall mon. Monoid mon => (message -> mon) -> mon) -> m ()
logMessageFree inj = lift (logMessageFree inj)
vs
class Monad m => MonadLog m where
type MonadLogMsg m
logMessageFree :: (forall n. Monoid n => (MonadLogMsg m -> n) -> n) -> m ()
default logMessageFree :: (m ~ t n, MonadTrans t, MonadLog n) => (forall mon. Monoid mon => (MonadLogMsg n -> mon) -> mon) -> m ()
logMessageFree inj = lift (logMessageFree inj)
The former requires UndecideableInstances
when deriving MonadLog
for bespoke monad transformers; a contrived example (pseudo code):
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizeNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeclasses #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
-- Required, because instance head for `MonadLog` does not get smaller
-- when deriving `MonadLog` for monad transformers that inherit the
--`MonadLog` instance from their base monad
{-# LANGUAGE UndecidableInstances #-}
import Control.Monad.State
import Control.Monad.Trans
newtype MyT m a = MyT { unMyT :: StateT Int m a }
deriving newtype (Functor, Applicative, Monad, MonadTrans, MonadState Int)
-- Requires {-# LANGUAGE UndecidableInstances #-}
deriving newtype MonadLog msg m => MonadLog msg (MyT m)
I am not "against" using UndecidableInstances
here, as I know in this case it's "safe" since the functional dependency fixes the msg
type and thus the only typeclass parameter that needs to shrink is the m
in MonadLog msg m
. However, I think that it might be avoidable if the associated type family approach is used. I haven't actually tried it myself, but I'm wondering if this is something you thought about and decided against for some interesting reason-- a reason I'm interested in learning about!
After toying around with the idea, I find that perhaps mapLogMessage
and friends wouldn't manifest as easily if at all 🤔Such a change would require a bit of work on the part of LoggingT
, Handler
, etc which I didn't foresee. Perhaps the ease of embedding one LoggingT message
inside of a MonadLog message'
with (message -> message')
function could be the sole reason to use this approach... I haven't dug too deeply yet to confirm.
Thanks for your response, feel free to close or leave open as you see fit; perhaps if left open it will evoke some insightful response from the community. I'll keep hacking away at swapping out the approach, but I'm getting stuck at the wrapping of arbitrary log message values with WithSeverity
, since the refactored MonadLog
class no longer exposes the msg
type variable, instead hiding it behind the instance definition; thus I can't figure out a sensible way to change the type signature of e.g. logDebug :: MonadLog (WithSeverity a) => a -> m ()
into something coherent with the new approach.
You can still bring the associated type into the constraint:
logDebug :: (MonadLog m, MessageType m ~ WithSeverity a) => a -> m ()
That said, I will close this because I won't be changing the API anyway - unless we find a very compelling reason.