Say I have to implement a function
f :: Foo -> ReaderT Bar IO Baz
that I have to pass to a consumer (i.e. I'll call c f
)
where Foo
/Bar
/Baz
are imposed the consumer of the function, and that the consumer will call that function repeatedly with different Foo
s.
Do I have any chance of keeping some local state between successive calls to f
?
In some way, I've already achieved that via an IORef
, by changing the signature of f
f :: IORef S -> Foo -> ReaderT Client IO a
and partially applying it to some state I initialize beforehand:
s <- initState :: IO (IORef S)
c $ f s
This way, the fully applied f
can alter the s
tate in the IO
monad via atomicModifyIORef'
and some u
pdate function:
f s x = do
liftIO $ atomicModifyIORef' s u
-- ...
However, the solution above came natural for me because that s
tate is truly a global mutable state that is modified not just by calls to f
, but also by some other part of the program that runs concurrently with f
.
But now I'm in need of f
to have some private state that is not meant to be modified by anyone else. This makes me think of the StateT
transformer, but I don't really know I can apply it in this case.
The practical usecase at hand is that I want a stateful notify
function in the context of implementing a notification server. See here for a toy example of the implementation. In this scenario, Foo -> ReaderT Bar IO Baz
is actually MethodCall -> ReaderT Client IO Reply
(ReaderT Client IO a
is DBusR a
).
As you can see, whatever I do with notify
, I must end up passing an MethodCall -> DBusR Reply
function to export
the client
, and that function will be called multiple times, and is expected to return everytime, so it appears to me that the only way to keep state in it is its closure, i.e. I have to provide notify
one more argument before MethodCall
, and partially apply it to the initial state, as I explained above. And the only way to have that state change every time a MethodCall
is passed, is to have the first additional argument be a truly mutable state, e.g. the IORef
I mentioned above.
Is this it?
Imagine that the signature of f
was:
f :: Foo -> Reader Bar Baz
and you somehow "used StateT
" in f
to thread a local state through a sequence of calls to f
. That would mean that two consecutive calls to f
with the same Foo
argument with the resulting reader run using the same Bar
argument:
let baz1 = runReader (f foo) bar
baz2 = runReader (f foo) bar
in ...
could result in two different baz1
and baz2
results, depending on the change in local state, right? In other words, it would be a violation of referential transparency.
So, the only reason you can carry a state through f :: Foo -> ReaderT Bar IO Baz
is because you have its base monad IO
available, so any solution you come up with is going to have to use IO
effects to maintain the state.
However, it's easy enough to use StateT
within the definition of f
, while actually maintaining the state as an IORef
that's private to f
. If you write your core f
logic using StateT
:
f :: Foo -> StateT S (ReaderT Client IO) a
f foo = do ...
you can build a sort of f
-factory:
f_factory :: IO (Foo -> ReaderT Client IO a)
f_factory = do
sref <- newIORef initS
pure $ \foo -> do
s <- liftIO $ readIORef sref
(r, s') <- runStateT (f foo) s
liftIO $ writeIORef sref s'
pure r
and in IO, you can write:
f_with_state <- f_factory
c f_with_state -- pass to consumer
Note that, since the sref
was created within f_factory
, it is included in the resulting closure f_with_state
but not available anywhere else. And, in the core definition of f
, you can just use get
, put
, modify
, etc., without worrying about the fact that the actual f_with_state
function is restoring and saving the state in the private sref
IORef.