fimad / prometheus-haskell

Haskell client library for exposing prometheus.io metrics.
84 stars 48 forks source link

MonadMonitor vs MonadIO #56

Closed torgeirsh closed 4 years ago

torgeirsh commented 4 years ago

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.

ocharles commented 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.

ocharles commented 4 years ago

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!

torgeirsh commented 4 years ago

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...

parsonsmatt commented 2 years ago

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.

torgeirsh commented 2 years ago

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.

torgeirsh commented 2 years ago

I just realised that it's possible to work around that by wrapping calls that require MonadMonitor in liftIO. It feels so obvious now... 😅