2
votes

I am trying to write a ‘logger’ Monad Transformer. Other Monad Transformers will then be applied to it, to form a more complex monad. I want the logger function to work on all these monads, so I wrote a typeclass as follows.

class Logger e m | m -> e where
    logMessage :: e -> m ()

The reason I use Functional Dependencies here, is that the monad m will explicitly contain the type e (as what it is with State monad), which stands for the message type.
The transformer ET is made an instance of typeclass Logger.

data ET e m a = ET { ... }
instance Monad m => Monad (ET e m) where
    logMessage msg = ...
instance Monad m => Logger e (ET e m) where
    logMessage msg = ...

Now, I want the monad T1 (T2 ... (ET m)) (which has an ET in the transformer chain) to be an instance of typeclass Logger, but it failed to compile. Below is the code.

instance (Logger e m, MonadTrans t) => Logger e (t m) where
    logMessage = lift . logMessage

I thought that since t is only a Monad Transformer, and m is guaranteed to uniquely determine e, then t m should also uniquely determine e. But the compiler seems to think differently.

Test.hs:43:10: error:
? Illegal instance declaration for ‘Logger e (t m)’
    The coverage condition fails in class ‘Logger’
      for functional dependency: ‘m -> e’
    Reason: lhs type ‘t m’ does not determine rhs type ‘e’
    Un-determined variable: e
    Using UndecidableInstances might help
? In the instance declaration for ‘MonadException e (t m)’
   |
43 | instance (Logger e m, MonadTrans t) => Logger e (t m) where
   |          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Failed, no modules loaded.

Can anyone explain how the extension FunctionalDependencies works, as well as how to solve this problem?
The compiler I use is The Glorious Glasgow Haskell Compilation System, version 8.2.2, on Windows 10.

2
Do you actually have UndecidableInstances enabled? It's almost always necessary with FunctionalDependencie, and the error message suggests you don't.Carl
@Carl I use FunDeps quite a lot, and I don't think I've never used UndecidableInstances.Paul Johnson
I guess it only comes up when the instances are polymorphic - but that's still an exceptionally common case. Basically every mtl-style instance, for example.Carl
@Carl, I am a newbie on such extensions, and someone else had warned me that I had better not enable an extension unless I know exacly what it does, so I just came here to get some suggestions.Ruifeng Xie
@Krantz That's a good general policy. As an introduction: UndecidableInstances allows you to compile instances with a fully-polymorphic argument. It doesn't break any code that already works, it doesn't allow new bugs.The only downside is that if you have an instance that actually loops during type-checking, it will take longer for the compiler to produce an error message - but it still will. Also, it's usually a bad idea to use it with a single-parameter type class. But for a MPTC with fundeps enabled, it's often necessary.Carl

2 Answers

1
votes

The trouble is that while m -> e it doesn't follow that t m -> e because the compiler doesn't know anything about what t might do.

What you have defined is not actually a monad transformer, its a class of Logger monads. Canonically, the mtl way of approaching this would be:

  1. Define a class (Monad m) => MonadLogger e m | m -> e (just rename your existing class).

  2. Define newtype LoggerT e m a = LoggerT(runLoggerT :: m <...>). This is your monad transformer. runLoggerT unwraps a LoggerT action and returns a value in the inner monad m. The details of what it returns are up to you.

  3. Create instances of Monad, MonadTrans and MonadLogger for LoggerT.

  4. Define type Logger e = Logger e Identity

  5. Create inductive instances for all the other mtl monad transformers.

If you take a look at the examples in the mtl library you should be able to see how its done.

1
votes

Many thanks to @Carl, this problem is solved.
When I turned on the language extension Undecidable Instances (by {-# LANGUAGE UndecidableInstances #-}), this error message disappeared.
Though still I wonder why this extension is needed, for now, it really makes the code compile.