我有这个多态代码(请参阅this question),其中包含用于模型和客户端的通用monad:
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"
这是Client
的漂亮印刷解释器,它通过Writer monad解释日志中的动作:
type Printer = Writer [String]
instance Client Printer where
addServer :: String -> Printer ()
addServer srv = tell [" add server " ++ srv ++ "to the client"]
Model
的翻译很困难。我尝试了几件事,每件事都会导致自己的错误:
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
某种程度上,我需要说c a
中的act
现在是Printer a
。
也许我需要在Model类中有两个参数-Model monad的m
和Client monad的c
,并且Model类还应该定义函数clientToModel :: c a -> m a
?
是否有办法使模型和客户端脱钩?我可能仍需要每对clientToModel :: c a -> m a
吗?
我感谢您的建议。谢谢!
问题是act
的类型签名保证它可以在any客户端上运行,但是在这里,您试图限制它仅在名为Printer
的特定客户端上运行。这违反了Model
类型类的定义。
您显然想遵循的通常模式是在同一个monad上同时定义Model
和Client
,如下所示:
class Monad m => Model m where
act :: String -> m a -> m a
class Monad m => Client m where
addServer :: String -> m ()
[这具有很好的,易于理解的语义,即act
和addServer
都是“环境上下文”操作,“在monad m
中可用”。它们几乎就像“全局函数”,但仍然是可模拟的。
然后Printer
可能是这种单子的一个示例,同时实现Client
和Model
。然后您的生产堆栈-例如ReaderT Config IO
或您拥有的任何东西-可能是此类monad的另一个示例。
但是,如果您坚持在不同的单子上定义Model
和Client
,则使类型起作用的唯一方法是将Client c
约束从act
的签名提升为[[ C0]类别:
Model
其含义是“ 每个“模型”单子都与一组特定的“客户端”单子一起工作,而不仅仅是随机的“客户端”单子。]]。
然后您可以这样定义class (Monad m, Client c) => Model m c where
act :: String -> c a -> m a
实例:
Printer
并且类型将起作用。
话虽如此,我想再次重申,您决定在不同单子上定义instance Model Printer Printer where
act name action = do
tell [name ++ ":"]
action
和Client
的决定对我来说是一种气味。我强烈建议您按照上述建议重新考虑设计。