你的泛化尝试act :: Client c => String -> c a -> m a
从技术上讲是正确的:它实际上是原始代码的翻译,但是替换State ModelData
with m
and State ClientData
with c
.
发生错误是因为现在“客户端”可以是任何东西,调用者scenario1
无法指定它应该是什么。
你看,为了确定哪个版本addServer
要调用,编译器必须知道什么c
是,但无处可推断!c
既不会出现在函数参数中,也不会出现在返回类型中。所以从技术上来说它绝对可以是任何东西,它完全隐藏在里面scenario1
。但“绝对任何”对于编译器来说还不够好,因为选择c
确定哪个版本addServer
被调用,这将决定程序的行为。
这是同一问题的较小版本:
f :: String -> String
f str = show (read str)
这同样不会编译,因为编译器不知道哪个版本show
and read
打电话。
你有几个选择。
First, if scenario1
它本身知道要使用哪个客户端,它可以通过使用来说明TypeApplications
:
scenario1 :: Model m => m ()
scenario1 = do
act "Alice" $ addServer @(State ClientData) "https://example.com"
Second, scenario1
可以将此任务卸载给调用它的任何人。为此,您需要声明一个通用变量c
即使它没有出现在任何参数或参数中。这可以通过以下方式完成ExplicitForAll
:
scenario1 :: forall c m. (Client c, Model m) => m ()
scenario1 = do
act "Alice" $ addServer @c "https://example.com"
(请注意,你仍然需要做@c
让编译器知道哪个版本addServer
使用;为了能够做到这一点,你需要ScopedTypeVariables
, 包括ExplicitForAll
)
那么消费者将不得不做这样的事情:
let server = scenario1 @(State ClientData)
Finally,如果由于某种原因你无法使用TypeApplications
, ExplicitForAll
, or ScopedTypeVariables
,你可以做同样事情的穷人版本 - 使用额外的虚拟参数来引入类型变量(这是以前的做法):
class Monad c => Client c where
addServer :: Proxy c -> String -> c ()
scenario1 :: (Client c, Model m) => Proxy c -> m ()
scenario1 proxyC = do
act "Alice" $ addServer proxyC "https://example.com"
(请注意,类方法本身现在也获取了一个虚拟参数;否则将再次无法调用它)
那么消费者将不得不做这件丑陋的事情:
let server = scenario1 (Proxy :: Proxy (State ClientData))