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
act
promises 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 theModel
type class.The usual pattern that you're apparently trying to follow is to define both
Model
andClient
on the same monad, like this:This has the nice, easily understood semantics that both
act
andaddServer
are "ambient context" operations that are "available in the monadm
". They're almost like "global functions", yet still mockable.Then
Printer
could be one example of such monad, implementing bothClient
andModel
. And then your production stack - likeReaderT Config IO
or whatever you have - could be another example of such monad.However, if you insist on
Model
andClient
being defined on different monads, the only way to make the types work is to lift theClient c
constraint from the signature ofact
to the signature of theModel
class: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:And the types will work.
Having said that, I want to reiterate once again that your decision to define
Client
andModel
on different monads is a smell to me. I strongly recommend that you reconsider your design as suggested above.