Mixing Commuting Monads

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. , , .

?

+4

Source: https://habr.com/ru/post/1673725/


All Articles