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:
instance Model Printer where
act :: String -> Printer a -> Printer a
act name action = do
tell [name ++ ":"]
action
instance Model Printer where
act :: forall a. String -> Printer a -> Printer a
act name action = do
tell [name ++ ":"]
action @(Printer a)
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 act
promises that it would work on any client, but here you're trying to constrain it to work only on the specific client called Printer
. This violates the definition of the Model
type class.
The usual pattern that you're apparently trying to follow is to define both Model
and Client
on the same monad, like this:
class Monad m => Model m where
act :: String -> m a -> m a
class Monad m => Client m where
addServer :: String -> m ()
This has the nice, easily understood semantics that both act
and addServer
are "ambient context" operations that are "available in the monad m
". They're almost like "global functions", yet still mockable.
Then Printer
could be one example of such monad, implementing both Client
and Model
. And then your production stack - like ReaderT Config IO
or whatever you have - could be another example of such monad.
However, if you insist on Model
and Client
being defined on different monads, the only way to make the types work is to lift the Client c
constraint from the signature of act
to the signature of the Model
class:
class (Monad m, Client c) => Model m c where
act :: String -> c a -> m a
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 Printer
instance like this:
instance Model Printer Printer where
act name action = do
tell [name ++ ":"]
action
And the types will work.
Having said that, I want to reiterate once again that your decision to define Client
and Model
on different monads is a smell to me. I strongly recommend that you reconsider your design as suggested above.