When I implemented my notification server using DBus, what I needed is to call makeMethod with a third argument being a stateful computation (because I needed to keep track of the most recently issued notification ID).
However, what makeMethod accepts as its 3rd argument is a MethodCall -> DBusR Reply, and the issue is that DBusR Reply is simply an alias for ReaderT Client IO Reply, whereas I wanted to have the power of StateT TheState (ReaderT Client m) Reply. What I ended up doing is writing this function:
state2IORef :: MonadIO m => (a -> StateT s m c) -> s -> IO (a -> m c)
state2IORef statefulComp s0 = do
s0ref <- liftIO $ newIORef s0
return $ \a -> do
(r, s) <- runStateT (statefulComp a) =<< liftIO (readIORef s0ref)
liftIO $ writeIORef s0ref s
pure r
which accepts the StateT ... computation and the initial value of the state, and returns a computation of the underlying monadic type, but using IO and an IORef to take care of the state evolution.
At the call site, I can do
func <- state2IORef myStatefulComp initialState
-- func has the type that makeMethod requires
The “advantage” is that myStatefulComp doesn’t need to liftIO for doing state management, the disadvantage is that the above looks a bit clunky.
Is the above approach overkill? Does anybody know what the recommended/consolidated approach is in such scenarii?
Do you want to share the state between multiple invocations of func? If so then I think this is fine. If not then you can use
state2IORef2 :: MonadIO m => (a -> StateT s m c) -> s -> a -> m c
state2IORef2 statefulComp s0 a = do
s0ref <- liftIO $ newIORef s0
(r, s) <- runStateT (statefulComp a) =<< liftIO (readIORef s0ref)
liftIO $ writeIORef s0ref s
pure r
I’m not sure there is one single recommended approach, but I developed Bluefin to tackle these kinds of issues, so if I were working with dbus I’d make a Bluefin like this:
-- Abstract types: constructors not exposed
newtype DBusH (e :: Effects) = MkDBusH DBus.Client
newtype Method (e :: EFfects) = MkMethod (DBus.Method)
makeMethod
:: MemberName
-> Signature -- ^ Input parameter signature
-> Signature -- ^ Output parameter signature
-> (forall e. MethodCall -> DBusH e -> Eff (e :& es) Reply)
-> Method es
(There are different ways to cook this, and some implementation details to take care of, but they all work out roughly the same. dbus is written in ReaderT of IO style, so it should be fairly easy to wrap.)
Then myStatefulComp would be something whose type contains a State argument, for example
myStatefulComp ::
(e1 :> es, e2 :> es, e3 :> es) =>
State s e1 ->
Whatever e2 ->
MethodCall ->
DBusH e3 ->
Eff es r
and you can use it like
let method =
makeMethod
memberName
sig1
sig2
(myStatefulComp myState myWhatever
Do you want to share the state between multiple invocations of func ?
Yes, that’s what I do.
if I were working with dbus
Do I understand correctly that in what follows you’re suggesting an improvement for the dbus library (via your proposed code or another of those “different ways to cook this”)?
I think I’d say it’s a slightly different alternative API that puts the dbus library into a more “Bluefin style” (but since it’s written in ReaderT of IO style it’s already pretty close). The “different ways to cook this” refers to a few slightly different possible implementations of the Bluefin skin that could be put on top of dbus.
Yes, it’s overkill. The approach I would recommend here is to keep it simple: just use the IORef directly. There are of course other approaches like effect systems, but they’re also overkill if it’s just for this.
The “advantage” of your approach is nothing that can’t be achieved just by binding convenient local names, e.g. with
mkState :: MonadIO m => s -> IO (m s, s -> m ())
mkState s = newIORef s <&> \r ->
( liftIO $ readIORef r
, liftIO . writeIORef r
)
Apart from introducing unwarranted complexity, there are other issues with the approach if the context is concurrent; the read/write isn’t atomic so updates can get lost. In that case, you’ll want to use something like
atomic :: MonadIO m => IORef s -> State s a -> m a
atomic r update = liftIO $ atomicModifyIORef' r (swap . runState update)
or
locking :: MVar s -> StateT s (ReaderT r IO) a -> ReaderT r IO a
locking mv update = ReaderT \r ->
modifyMVar mv \s ->
swap <$> runReaderT (runStateT update s) r