Closed torgeirsh closed 4 years ago
It's the ()
that is significant in MonadMonitor
. liftIO
has a more general type - liftIO :: MonadIO m => IO a -> m a
, but doIO :: MonadMonitor m => IO () -> m ()
. The reason this is significant is because it allows implementations like:
newtype MonitorT m a = MkMonitorT (WriterT [IO ()] m a)
deriving (Applicative, Functor, Monad, MonadTrans)
instance Monad m => MonadMonitor (MonitorT m) where
doIO f = MkMonitorT $ tell [f]
I've never been entirely fond of that type class, but that's why things are how they are atm.
I've closed this as I've hopefully answered the question, but if you want to continue the conversation please feel free to add more comments!
I see! It would be nice if an instance could be provided for free when a MonadIO is available, but instance MonadIO m => MonadMonitor m is undecidable with a lot of overlaps, so that's not a good idea...
Yeah, unfortunately the point of the class seems to be a way to defer/record the IO
actions that perform the things themselves. While almost all IO
-capable transformers will probably want to do this on-time, it's also reasonable to keep it separated as the "interface" for the class.
It's an interesting point in the design space. Another way of writing this would be something like the MonadMetrics
type class:
class Monad m => MonadMetrics m where
getMetrics :: m Metrics
The library API is then f :: (MonadIO m, MonadMetrics m) => ...
.
A further point in the design space would be something that removes the IO
entirely from the signature.
class Monad m => MonadMetrics m where
counter :: Text -> Int -> m ()
distribution :: Text -> Double -> m ()
-- etc...
The design in this allows the API to be a bit simpler, while still relying on IO
to actually do the work.
I was pretty mad about it at first, but now that I've spent a bit more time understanding it, it's a nice trick.
I think the "pure metrics" aspect is a bit unwieldy, since you need to bubble the [IO ()]
up to some IO
action to actually record them. But bubbling parameters around isn't exactly unusual.
I understand the concern for pure monitoring, it's just unfortunate that it means losing support for "real" monitoring when all you have is MonadIO.
I just realised that it's possible to work around that by wrapping calls that require MonadMonitor in liftIO. It feels so obvious now... 😅
At first glance, the MonadMonitor typeclass looks very similar to MonadIO, but maybe I'm missing something? Could MonadIO replace MonadMonitor? It would reduce the impedance mismatch with other libraries, whose monads often come with MonadIO instances, but not MonadMonitor, thus requiring an orphan instance or newtype wrapper.