一个函数中两个多态 类 的解释器

Interpreters for two polymorphic classes in one function

我有这个多态代码(见 ),带有模型和客户端的通用单子:

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 的解释器很难。我尝试了几件事,每件事都会导致自己的错误:

  1. "Couldn't match type ‘c’":
instance Model Printer where
  act :: String -> Printer a -> Printer a
  act name action = do
    tell [name ++ ":"]
    action
  1. “`无法将‘Printer a’类型的表达式应用于可见类型参数‘(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

不知何故我需要告诉 act 中的 c a 现在是 Printer a

也许我需要在模型 class 中有两个参数 - m 用于模型 monad 和 c 用于客户端 monad,模型 class 也应该定义函数 clientToModel :: c a -> m a?

有没有办法让模型和客户端解耦?我可能还需要每对 clientToModel :: c a -> m a

感谢您的建议。谢谢!

问题是 act 的类型签名承诺它可以在 任何 客户端上工作,但在这里你试图限制它只在 上工作特定客户端称为 Printer。这违反了 Model 类型的定义 class.


您显然试图遵循的通常模式是在同一个 monad 上同时定义 ModelClient,如下所示:

class Monad m => Model m where
  act :: String -> m a -> m a

class Monad m => Client m where
  addServer :: String -> m ()

actaddServer 都是 "ambient context" 操作,它们是 "available in the monad m",这具有很好的、易于理解的语义。他们几乎像 "global functions",但仍然可以嘲笑。

那么 Printer 可能是这种 monad 的一个例子,实现了 ClientModel。然后你的生产堆栈——比如 ReaderT Config IO 或你拥有的任何东西——可能是这种 monad 的另一个例子。


但是,如果您坚持在不同的 monad 上定义 ModelClient,则使这些类型起作用的唯一方法是解除 Client c 签名中的约束actModel 的签名 class:

class (Monad m, Client c) => Model m c where
  act :: String -> c a -> m a

这意味着“每个 "model" monad 与一组特定的 "client" monad 一起工作,但不仅仅是任何随机的 "client" monad".

然后你可以这样定义 Printer 实例:

instance Model Printer Printer where
  act name action = do
    tell [name ++ ":"]
    action

这些类型会起作用。


话虽如此,我想再次重申,您决定在不同的 monad 上定义 ClientModel 对我来说是一种异味。我强烈建议您按照上面的建议重新考虑您的设计。