felixmulder/haskell-in-production

How could this capability model/pattern handle callbacks

Closed this issue ยท 6 comments

Maybe this is a beginner question and the solution is obvious, but how can this pattern work with callbacks? I'm trying to get something similar to the following to work for hours ๐Ÿ˜ข REALLY!

class Monad m => Database m where
  registerCallback :: (String -> m ()) -> m ()

data DatabaseImpl m = DatabaseImpl
  { doRegisterCallback :: (String -> m ()) -> m ()
  }

instance
  ( Has (DatabaseImpl m) r
  , Monad m
  ) => Database (ReaderT r m) where
    registerCallback cb = asks getter >>= \(DatabaseImpl doRegisterCallback) -> lift . doRegisterCallback  $ cb

While I understand that this leads to the cannot construct the infinite type: m ~ ReaderT r m problem, I have no idea how to avoid this. I would not like to remove the recursion completely since I would like to use other aspects - e.g. logging - both in data access (concrete database implementation) and in the callback (to allow the client code to add logging as well).

I've tried an approach like https://stackoverflow.com/questions/50139963/monadreader-foo-m-m-results-in-infinite-type-from-functional-dependency but failed to implement this properly.

So my guess is still that some of my assumptions are wrong - as a Haskell beginner with an OO background - and the solution is pretty simple; maybe just a few type adaptations are necessary; but I really need some support here. And since the code here is part of a tutorial I hope I'm allowed to ask for help here - even if this is not a classical issue kind of thing. Maybe this "real world problem" could improve further parts of the tutorial or at least this issue could be used as a reference for others encountering similar problems. Thanks in advance for any hint - and also thanks for this great tutorial bringing me to start to refactor my application towards a clear capability model.

Your problem becomes more obvious when spelling out the instance type:

instance
  ( Has (DatabaseImpl m) r
  , Monad m
  ) => Database (ReaderT r m) where
--registerCallback :: (String -> ReaderT r m ()) -> ReaderT r m ()
  registerCallback cb = do
    r <- ask
    let
      run :: (String -> ReaderT r m ()) -> (String -> m ())
      run f s = runReaderT (f s) r

      reg :: (String -> m ()) -> m ()
      reg = doRegisterCallback . getter $ r

    lift . reg $ run cb

It meant that the type that didn't line up was cb - it was (String -> ReaderT r m ()) whereas you required (String -> m ()).

I hope that makes sense. There's probably less convoluted ways to do this, but an initial hackety hack this morning revealed this solution.

Thanks a lot! For sure your solution works as expected - but to be honest I didn't understand the code to a level so that I'm able to adapt it to a only a little more complex situation. So may I ask you one additional question - so that I am maybe able to recognize the pattern behind your solution? How would this look like for a additional nested callback?

class Monad m => Database m where
  registerCallback :: ((String -> m ()) -> m ()) -> m ()

Do the same exercise as I showed you above ๐Ÿ™‚

If you have turn on {-# LANGUAGE InstanceSigs #-} you can add the signature yourself to see what the additional ms turn into ๐Ÿ™‚

Thanks - it's relatively simple... once figured out ๐Ÿ˜ƒ

instance (Has (BrokerImplComplex m) r, Monad m) => BrokerComplex (ReaderT r m) where
  actComplex :: ((String -> ReaderT r m Char) -> ReaderT r m Int) -> ReaderT r m Bool
  actComplex cb = do
    r <- ask
    let
      run :: ((String -> ReaderT r m Char) -> ReaderT r m Int) -> (String -> m Char) -> m Int
      run f s = runReaderT (f (lift . s)) r
      reg :: ((String -> m Char) -> m Int) -> m Bool
      reg = doActComplex . getter $ r
    lift (reg $ run cb)

Once again - THANK YOU - for your assistance - your blog post was a great inspiration for me as a haskell beginner.

Happy to see you solved it! ๐Ÿ˜„

I'll try to get started on the next post during the weekend ๐Ÿ™