I read about the final tag-free encodings that gave me a new perspective on mtl. Namely, the interpreter for dsl defines a specific monad stack. This makes it more obvious to me why n ^ 2 instances are needed.
Obviously, there are also commutative monads for which this is not needed. The most notable use case would be the separation of IOs to make it easier to reason and test.My first idea was to parameterize MonadIO with an implementation:
class (Monad m, Monad (Eff m)) => MonadEff m where
type Eff m :: (* -> *)
liftEff :: Eff m a -> m a
Surprisingly, this seems to be excellent. This would allow to identify instances of lifting once.
instance MonadEff IO where
type Eff IO = IO
liftEff = id
instance MonadEff Tracer where
type Eff Tracer = Tracer
liftEff = id
instance MonadEff m => MonadEff (ExceptT e m) where
type Eff (ExceptT a m) = Eff m
liftEff = lift . liftEff
-- ...
And it's easy to create effects:
g :: (MonadEff m, MonadError String m, ConsoleEff (Eff m)) => m ()
g = liftEff (put "foo") >> throwError "bar"
h :: (MonadEff m, ConsoleEff (Eff m), FileEff (Eff m)) => m ()
h = liftEff $ do
l1 <- get
l2 <- get
c <- read l2
put $ c ++ l1
class FileEff repr where
read :: FilePath -> repr String
write :: FilePath -> String -> repr ()
class ConsoleEff repr where
put :: String -> repr ()
get :: repr String
instance ConsoleEff IO where
get = getLine
put s = putStrLn s
instance ConsoleEff Tracer where
put s = W.tell [s] >> return ()
get = do
i <- S.get
S.put (i+1)
W.tell $ ["putL '" ++ show i ++ "'"]
return $ "$" ++ show i ++ "(get)"
It also allows you to use various views for testing, etc.
exec :: IO a -> IO a
exec = id
trace :: Tracer a -> [String]
trace (Tracer f) = snd . runWriter $ evalStateT f (0::Int)
, , , , mtl. , , .
?