Search code examples
haskellmonad-transformers

How to add Mask to a monadic stack


I am trying to use the bracket function from Exception.Safe Package which has a return type of

forall m a b c. MonadMask m => m a -> (a -> m b) -> (a -> m c) -> m c

This implies that my monad stack must have the Mask monad added to the stack ? My calling function looks like this

sendMessage :: String -> Config.KafkaP (Either KafkaError ())
sendMessage msg=do
      getProperties <- producerProps
      let
        mkProducer =  newProducer getProperties
        clProducer (Left _)     = return ()
        clProducer (Right prod) = closeProducer prod
        runHandler (Left err)   = return $ Left err
        runHandler (Right prod) = messageSender prod msg
      res <- bracket mkProducer clProducer runHandler
      return res

And config.KakfaP has the type

ReaderT ProducerConfig IO a

And the error I get is ,

No instance for (exceptions-0.10.0:Control.Monad.Catch.MonadMask
                         Config.KafkaP)
        arising from a use of ‘bracket’

Does this mean the monad stack needs to be something like this

Mask (ReaderT ProducerConfig IO a) 

Ideally I would want the function to return what the run Handler returns which is Config.KafkaP (Either KafkaError ()) ,or anything more robust .

Adding a solution based on an answer

sendMessage :: String -> Config.KafkaP (Either KafkaError ())
sendMessage msg=do
      getProperties <- producerProps
      let
        mkProducer =  newProducer getProperties  
        --newProducer :: MonadIO m => ProducerProperties -> m (Either KafkaError KafkaProducer)
        --mkProducer :: Config.KafkaP (Either KafkaError KafkaProducer)
        clProducer (Left _)     = return ()
        clProducer (Right prod) = closeProducer prod
        --closeProducer :: MonadIO m => KafkaProducer -> m ()
        --clProducer :: Config.KafkaP (Either () ())  -- ?? 
        runHandler (Left err)   = return $ Left err
        runHandler (Right prod) = messageSender prod msg
        --messageSender :: KafkaProducer -> String -> Config.KafkaP (Either KafkaError ())
        --runHandler :: Config.KafkaP (Either KafkaError ()) -- ?? 
      Config.KafkaP $ bracket (Config.runK  mkProducer) (Config.runK .clProducer) (Config.runK .runHandler)

Solution

  • If you were using the type ReaderT ProducerConfig IO a directly, there wouldn't be a problem, because the exceptions package provides an instance

    MonadMask IO

    That says you can use bracket with IO, and another instance

    MonadMask m => MonadMask (ReaderT r m)

    That says that if the base monad is an instance of MonadMask, then ReaderT over that monad is also an instance of MonadMask.

    Notice that MonadMask is not a transformer that is part of the monad stack. Instead, it is a constraint that says "this monad stack supports masking / bracketing operations".


    If you were using a type synonym like

    type KafkaP a = ReaderT ProducerConfig IO a
    

    there wouldn't be a problem either, because type synonyms don't create a new type, they just give an alias to an exiting one. One can still make use of all the existing typeclass instances for the type.


    You mention in the comments that KafkaP is a newtype. A newtype is a cheap way of creating, well, a new type out of another. It's basically a constructor that holds a value of the original type.

    And that's the problem. Because it is a new type, it doesn't share automatically all the typeclass instances of the old one. In fact, having different typeclass instances in newtypes is one of the main motivations for using newtypes!

    What can be done? Well, supposing the newtype constructor is exported (sometimes they are hidden for purposes of encapsulation) you could unwrap the KafkaPs actions into ReaderT ProducerConfig IO a before sending them into bracket, and then re-wrap the result into KafkaP again. Some of the parameters are KafkaP-returning functions, so you'll probably need to throw in some function composition as well. Perhaps something like (assuming KafkaP is the name of the constructor, and runKafkaP the name of the corresponding accessor):

    KafkaP $ bracket (runKafkaP mkProducer) (runKafkaP . clProducer) (runKafkaP . runHandler)
    

    All this wrapping and unwrapping is tedious; sometimes using coerce from Data.Coerce can help. But this only works when the newtype constructor is exported.


    You might also think of defining your own MonadMask instance for KafkaP using the technique above. This can be done, but instances which are defined neither in the module that defines the typeclass nor in the module that defines the type are called orphan instances and are somewhat frowned upon.