The function liftBaseOpDiscard
from monad-control seems to do the trick:
import Control.Monad.Trans.Control
type MyMonad a = ReaderT Int (StateT Int IO) a
onActivate' :: Widget Edit -> (Widget Edit -> MyMonad ()) -> MyMonad ()
onActivate' = liftBaseOpDiscard . onActivate
This function has a MonadBaseControl
constraint, but ReaderT
and StateT
on top IO
already have instances for that typeclass.
As the documentation for liftBaseOpDiscard
mentions, changes to the state inside the callback will be discarded.
MonadBaseControl
lets you temporarily hide the upper layers of a monad stack into a value of the base monad of the stack (liftBaseWith
) and afterwards pop them again, if needed (restoreM
).
Edit: If we need to preserve effects that take place inside the callback (like changes in the state) one solution is to "mimic" state by using an IORef
as the environment of a ReaderT
. Values written into the IORef
are not discarded. The monad-unlift
package is built around this idea. An example:
import Control.Monad.Trans.Unlift
import Control.Monad.Trans.RWS.Ref
import Data.IORef
-- use IORefs for the environment and the state
type MyMonad a = RWSRefT IORef IORef Int () Int IO a
onActivate' :: Widget Edit -> (Widget Edit -> MyMonad ()) -> MyMonad ()
onActivate' we f = do
-- the run function will unlift into IO
UnliftBase run <- askUnliftBase
-- There's no need to manually "restore" the stack using
-- restoreM, because the changes go through the IORefs
liftBase $ onActivate we (run . f)
The monad can be run afterwards using runRWSIORefT
.