I have this polymorphic code (see this question) with generic monads for model and client:
import Control.Monad.Writer
class Monad m => Model m where
act :: Client c => String -> c a -> m a
class Monad c => Client c where
addServer :: String -> c ()
scenario1 :: forall c m. (Client c, Model m) => m ()
scenario1 = do
act "Alice" $ addServer @c "https://example.com"
and this is the pretty-print interpreter for the Client that explains the actions in the log via Writer monad:
type Printer = Writer [String]
instance Client Printer where
addServer :: String -> Printer ()
addServer srv = tell [" add server " ++ srv ++ "to the client"]
Interpreter for the Model is difficult. I tried several things, each resulting in its own error:
- "Couldn't match type ‘c’":
instance Model Printer where
act :: String -> Printer a -> Printer a
act name action = do
tell [name ++ ":"]
action
- "`Cannot apply expression of type ‘Printer a’ to a visible type argument ‘(Printer a)’":
instance Model Printer where
act :: forall a. String -> Printer a -> Printer a
act name action = do
tell [name ++ ":"]
action @(Printer a)
- "Couldn't match type ‘c’ with ‘WriterT [String] Data.Functor.Identity.Identity’"
instance Model Printer where
act :: Client c => String -> c a -> Printer a
act name action = do
tell [name ++ ":"]
action
Somehow I need to tell that what was c a in act is now Printer a.
Maybe I need to have two parameters in the Model class - m for Model monad and c for Client monad, and Model class should also define the function clientToModel :: c a -> m a?
Is there a way to keep Model and Client decoupled? I probably would still need clientToModel :: c a -> m a for each pair?
I appreciate the advice. Thank you!
The problem is that the type signature of
actpromises that it would work on any client, but here you're trying to constrain it to work only on the specific client calledPrinter. This violates the definition of theModeltype class.The usual pattern that you're apparently trying to follow is to define both
ModelandClienton the same monad, like this:This has the nice, easily understood semantics that both
actandaddServerare "ambient context" operations that are "available in the monadm". They're almost like "global functions", yet still mockable.Then
Printercould be one example of such monad, implementing bothClientandModel. And then your production stack - likeReaderT Config IOor whatever you have - could be another example of such monad.However, if you insist on
ModelandClientbeing defined on different monads, the only way to make the types work is to lift theClient cconstraint from the signature ofactto the signature of theModelclass:Which would have the meaning of "every "model" monad works with a certain set of "client" monads, but not just any random "client" monad".
Then you could define the
Printerinstance like this:And the types will work.
Having said that, I want to reiterate once again that your decision to define
ClientandModelon different monads is a smell to me. I strongly recommend that you reconsider your design as suggested above.