I am currently experimenting with typeclasses & as an exercise, the ability to log in a variety of contexts (i.e. print to the console in the context of IO). I started off by implementing my Logger as a typeclass made up of various functions for logging with the idea in mind that I could define an instance for the IO monad but leave room for additional implementation in the context of other monads.
The end result is :
-- |Class / wrapper for convenient use within another monad.
class Logger m where
-- |Logs an error message /(prefixed with the '__[ERROR]__' tag)/
logError :: String -> m ()
-- |Logs a warning message /(prefixed with the '__[WARNING]__' tag)/
logWarning :: String -> m ()
-- |Logs a success message /(prefixed with the '__[SUCCESS]__' tag)/
logSuccess :: String -> m ()
-- |Logs an informative message /(prefixed with the '__[INFO]__' tag)/
logInfo :: String -> m ()
-- |Logs a regular message /(i.e with no prefix)/
logMsg :: String -> m ()
-- |Instance of logger in the IO monad
instance Logger IO where
logError = printError
logWarning = printWarning
logSuccess = printSuccess
logInfo = printInfo
logMsg = printMsg
-- |Instance of logger for a state
instance (MonadIO m) => Logger (StateT s m) where
logError = liftIO . printError
logWarning = liftIO . printWarning
logSuccess = liftIO . printSuccess
logInfo = liftIO . printInfo
logMsg = liftIO . printMsg
This seemed like a good idea at the time (and coming from an OOP background I am drawn to making everything into 'classes' when I probably shouldn't)
I have come to realise I could have just as easily defined my logging functions directly with type contraints and call it a day, e.g. :
logError :: (MonadIO m) => String -> m ()
logError = liftIO . printError
And so on for the other functions and I would have something that can be called in any IO-based monad...
Obviously, both solutions have got their benefits and their trade-offs.
Could my use-case of a typeclass for Logger be considered "abuse" or do I have the right idea in implementing it that way (my understanding is that type classes allow for ad hoc polymorphism which is what I had in mind).
One limitation I have read about & which I am still trying to fully conceptualise is the fact there can only be one instance of a typeclass for any given type, so in my case, I have already defined an instance for StateT which live in the IO monad, meaning I lose the ability to override for subsquent states with the same signature. I am aware of this caveat but I am having a hard time thinking of a situation where this would become a concrete problem.
On the flip side, the simple function-based approach is just as elegant to use although it does prevent overriding the behaviour without defining a brand new function to be used in a different context.
Should typeclasses only be used/written as a last resort when functions can just as easily do the job?
I would appreciate some insight and feedback on the two approaches.
Thanks in advance,
StateTinstance should have a different constraint:instance Logger m => Logger (StateT s m) where logError = lift . logError, etc. - dfeuer