Interpreters for two polymorphic classes in one function

107 views Asked by At

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:

  1. "Couldn't match type ‘c’":
instance Model Printer where
  act :: String -> Printer a -> Printer a
  act name action = do
    tell [name ++ ":"]
    action
  1. "`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)
  1. "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!

1

There are 1 answers

1
Fyodor Soikin On BEST ANSWER

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.