简体   繁体   中英

Replicating the concept of liftIO with a monadtransformer

I have trouble even explaining what I'm trying to do, please bear with me. I haven't really grasped type theory, or how to use forall so that is probably the reason why wording (and implementing) this is difficult for me. So basically I'm trying to replicate the behaviour of MonadIO with a MonadTransformer. MonadIO is defined as

class (Monad m) => MonadIO m where
    -- | Lift a computation from the 'IO' monad.
    liftIO :: IO a -> m a

-- | @since 4.9.0.0
instance MonadIO IO where
    liftIO = id

This code compiles (I haven't included the Applicative and Functor instances here though, but I can):

newtype Debugable m r = Debugable { runDebugable :: (DebugState -> m (DebugState, r)) }

instance MonadTrans (Debugable)  where
  lift action = Debugable $ \d -> do
    r <- action
    return (d, r)

class (Monad i, Monad m) => MonadDebug i m where
  liftDebug :: Debugable i a -> m a


instance (Monad m) => MonadDebug m (Debugable m) where
  liftDebug = id

I want to write a method like this, that should work in any MonadTransformer above MonadDebug in the transformer stack:

debug :: (MonadDebug i m) => String -> m ()
debug msg = liftDebug $ doDebug msg

(doDebug operates in the Debugable monad)

Of course this does not compile because the type variable i cannot be deduced (or something like that)... I tried a few things, but I don't really understand what the debug messages mean either. I don't even know if a generalized lifting of a MonadTransformer similar to liftIO is possible... Is it?

I suggest changing your goal slightly.

The idiomatic way to make this class is to expose all of the base transformer's operations as class methods, rather than exposing a lifting transformation. Like this:

class Monad m => MonadDebugable m where
    debug :: (DebugState -> (DebugState, r)) -> m r

instance Monad m => MonadDebugable (Debugable m) where
    debug f = Debugable (pure . f)

instance MonadDebugable m => MonadDebugable (ReaderT r m) where
    debug = lift . debug
-- and a dozen other instances for other transformers

Then you can offer derived operations, like, say,

observeCurrentState :: MonadDebugable m => m DebugState
observeCurrentState = debug (\s -> (s, s))

recordState :: MonadDebugable m => DebugState -> m ()
recordState s = debug (\_ -> (s, ()))

debugMsg :: MonadDebugable m => String -> m ()
debugMsg msg = debug (\s -> (s { messages = msg : messages s }, ())) -- or whatever

Then, in all your client code, where you would originally have written liftDebug foo , you simply write foo (but using operations like those above that are polymorphic over which exact monad stack you're using at the moment).

Of course, the obvious follow-up question is: what's the deal with MonadIO , then? Why doesn't IO follow this pattern? The answer there is that IO just has too many primitive operations to write a sensible class -- indeed, with the FFI, it's even possible for libraries and users to add new primitives. So we have to go with the slightly less desirable solution there, where we must annotate all our primitive calls with liftIO if we're not exactly in IO already. Skipping the annotations, like above, would be nicer, but we just can't have it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM